Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for video captions #915

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php
/**
* @copyright Copyright (c) 2021 Frederic Ruget (douzebis) <fred@atlant.is>
*
* @author Frederic Ruget (douzebis) <fred@atlant.is>
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

return [
'routes' => [
/** @see \OCA\Viewer\Controller\VideoController */
[ 'name' => 'video#getTracks', 'url' => '/video/tracks', 'verb' => 'GET', ]
]
];
176,518 changes: 176,515 additions & 3 deletions js/viewer-main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/viewer-main.js.map

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions lib/Controller/VideoController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php
/**
* @copyright Copyright (c) 2021 Frederic Ruget <fred@atlant.is>
*
* @author Frederic Ruget (douzebis) <fred@atlant.is>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Viewer\Controller;

use Exception;

use OCP\IRequest;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\APIController;
use OCP\L10N\IFactory;

use OCP\Files\File;
use OCP\Files\Node;
use OCP\Files\IRootFolder;
use OCP\IUserSession;

use OCA\Viewer\AppInfo\Application;

class VideoController extends APIController {

private IRootFolder $rootFolder;
private IFactory $l10nFactory;
private IUserSession $userSession;

public function __construct(
IRequest $request,
IRootFolder $rootFolder,
IFactory $l10nFactory,
IUserSession $userSession
){
parent::__construct(Application::APP_ID, $request);
$this->rootFolder = $rootFolder;
$this->l10nFactory = $l10nFactory;
$this->userSession = $userSession;
}

/**
* @NoAdminRequired
*/
public function getTracks(string $videoPath): DataResponse {
/**
* $videoPath is the path to the main video file, eg: 'mypath/movie.mkv'
* Return an array of found tracks associated with the main video.
* Each track is an object:
* - basename // basename of the track
* - language // language for the track
* - locale // (usually 2-letter) code for the language
*/
$locales = array_filter($this->l10nFactory->findAvailableLocales(),
function(array $v): bool {
// Discard codes that are more than 3 characters long
// Otherwise we get a list of 1500 candidate tracks !!
return strlen($v['code']) <= 3;
});
$video = $this->rootFolder
->getUserFolder($this->userSession->getUser()->getUID())
->get($videoPath);
$videoDir = $video->getParent();
$videoName = pathinfo($video->getFileInfo()->getPath())['filename'];
$candidateTracks = array_merge(
// List candidateTracks of the form 'video.<locale>.vtt'
array_map(function (array $locale) use ($videoName): array {
return [
'basename' => $videoName . '.' . $locale['code'] . '.vtt',
'language' => $locale['name'],
'locale' => $locale['code']
];
}, $locales),
// Add candidateTracks of the form '.video.<locale>.vtt' (dotted)
array_map(function (array $locale) use ($videoName): array {
return [
'basename' => '.' . $videoName . '.' . $locale['code'] . '.vtt',
'language' => $locale['name'],
'locale' => $locale['code']
];
}, $locales));

$dirContentNames = array_map(fn (Node $node) => $node->getName(), $videoDir->getDirectoryListing());

// Keep only tracks actually available in the video folder
$availableTracks = array_filter($candidateTracks, function(array $v) use ($dirContentNames): bool {
return in_array($v['basename'], $dirContentNames);
});
return new DataResponse($availableTracks);
}
}
42 changes: 40 additions & 2 deletions src/components/Videos.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
@canplay="doneLoading"
@loadedmetadata="onLoadedMetadata">

<track v-for="track in tracks"
:key="track.davPath"
:src="track.davPath"
:label="track.language"
kind="captions"
:srclang="track.locale">

<!-- Omitting `type` on purpose because most of the
browsers auto detect the appropriate codec.
Having it set force the browser to comply to
Expand All @@ -57,6 +64,11 @@ import Vue from 'vue'
import VuePlyr from '@skjnldsv/vue-plyr'
import '@skjnldsv/vue-plyr/dist/vue-plyr.css'
import logger from '../services/logger.js'
import { extractFilePaths } from '../utils/fileUtils'
import getFileList from '../services/FileList'
import { dirname } from '@nextcloud/paths'
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'

const liveExt = ['jpg', 'jpeg', 'png']
const liveExtRegex = new RegExp(`\\.(${liveExt.join('|')})$`, 'i')
Expand All @@ -66,6 +78,12 @@ Vue.use(VuePlyr)
export default {
name: 'Videos',

data() {
return {
tracks: [],
}
},

computed: {
livePhoto() {
return this.fileList.find(file => {
Expand All @@ -84,8 +102,8 @@ export default {
options() {
return {
autoplay: this.active === true,
// Used to reset the video streams https://github.com/sampotts/plyr#javascript-1
blankVideo: 'blank.mp4',
// Make sure plyr _reacts_ on caption updates
captions: { active: false, language: 'auto', update: true },
controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'fullscreen'],
loadSprite: false,
}
Expand Down Expand Up @@ -132,13 +150,33 @@ export default {
this.updateHeightWidth()
},

// Fetch caption tracks and build HTML5 block
async fetchTracks() {
try {
const { data } = await axios.get(
generateUrl('/apps/viewer/video/tracks'),
{ params: { videoPath: this.filename } }
)
const davDir = dirname(this.davPath)
this.tracks = Object.values(data).map(track => ({
davPath: davDir + '/' + track.basename,
language: track.language,
locale: track.locale,
}))
} catch (error) {
console.error('Unable to fetch subtitles', error)
this.tracks = []
}
},

donePlaying() {
// reset and show poster after play
this.$refs.video.autoplay = false
this.$refs.video.load()
},

onLoadedMetadata() {
this.fetchTracks()
this.updateVideoSize()
// Force any further loading once we have the metadata
if (!this.active) {
Expand Down