hls: initial commit

pull/221/head
Varun Patil 2022-11-08 20:08:30 -08:00
parent 32966d75af
commit 8a16deeec4
6 changed files with 187 additions and 2 deletions

View File

@ -57,6 +57,8 @@ return [
['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],
['name' => 'Video#transcode', 'url' => '/api/video/transcode/{fileid}/{profile}', 'verb' => 'GET'],
// Config API
['name' => 'Other#setUserConfig', 'url' => '/api/config/{key}', 'verb' => 'PUT'],

View File

@ -95,6 +95,11 @@ class PageController extends Controller
$policy->addAllowedWorkerSrcDomain("'self'");
$policy->addAllowedScriptDomain("'self'");
// Video player
$policy->addAllowedWorkerSrcDomain('blob:');
$policy->addAllowedScriptDomain('blob:');
$policy->addAllowedMediaDomain('blob:');
// Allow nominatim for metadata
$policy->addAllowedConnectDomain('nominatim.openstreetmap.org');
$policy->addAllowedFrameDomain('www.openstreetmap.org');

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Varun Patil <radialapps@gmail.com>
* @author Varun Patil <radialapps@gmail.com>
* @license AGPL-3.0-or-later
*
* 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\Memories\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\DataDisplayResponse;
class VideoController extends ApiBase
{
/**
* @NoAdminRequired
*
* @NoCSRFRequired
*
* Transcode a video to HLS by proxy
*
* @param string fileid
* @param string video profile
*
* @return JSONResponse an empty JSONResponse with respective http status code
*/
public function transcode(string $fileid, string $profile): Http\Response
{
$user = $this->userSession->getUser();
if (null === $user) {
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
}
// Make sure not running in read-only mode
if ($this->config->getSystemValue('memories.no_transcode', false)) {
return new JSONResponse(['message' => 'Transcoding disabled'], Http::STATUS_FORBIDDEN);
}
// Get file
$files = $this->rootFolder->getById($fileid);
if (count($files) === 0) {
return new JSONResponse(['message' => 'File not found'], Http::STATUS_NOT_FOUND);
}
$file = $files[0];
// Local files only for now
if (!$file->getStorage()->isLocal()) {
return new JSONResponse(['message' => 'External storage not supported'], Http::STATUS_FORBIDDEN);
}
// Get file path
$path = $file->getStorage()->getLocalFile($file->getInternalPath());
if (!$path || !file_exists($path)) {
return new JSONResponse(['message' => 'File not found'], Http::STATUS_NOT_FOUND);
}
// Make upstream request
$ch = curl_init("http://localhost:9999/vod/$path/$profile");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, 0);
$data = curl_exec($ch);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$returnCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Check data was received
if ($returnCode >= 400 || false === $data) {
return new JSONResponse(['message' => 'Transcode failed'], Http::STATUS_INTERNAL_SERVER_ERROR);
}
// Create and send response
$response = new DataDisplayResponse($data, Http::STATUS_OK, [
'Content-Type' => $contentType,
]);
$response->cacheFor(3600 * 24, false, false);
return $response;
}
}

42
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@nextcloud/paths": "^2.1.0",
"@nextcloud/sharing": "^0.1.0",
"@nextcloud/vue": "7.0.0",
"@silvermine/videojs-quality-selector": "^1.2.5",
"camelcase": "^7.0.0",
"filerobot-image-editor": "^4.3.7",
"justified-layout": "^4.1.0",
@ -2162,6 +2163,18 @@
"styled-components": "^5.1.0"
}
},
"node_modules/@silvermine/videojs-quality-selector": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@silvermine/videojs-quality-selector/-/videojs-quality-selector-1.2.5.tgz",
"integrity": "sha512-cielchUzL8r2EX01S7PfR54tTbxDZR53xIDJoUi9Wg6pM2X+ftdJD6XiIDVOjPlBoG94iuG9LJwUtjX5IhrWZQ==",
"dependencies": {
"class.extend": "0.9.1",
"underscore": "1.13.1"
},
"peerDependencies": {
"video.js": ">=6.0.0"
}
},
"node_modules/@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
@ -3539,6 +3552,11 @@
"resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz",
"integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA=="
},
"node_modules/class.extend": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/class.extend/-/class.extend-0.9.1.tgz",
"integrity": "sha512-Tzj+2kAkZs+iGiUOUoKvtj4c/SjeVdKZXg/NbLTGKu0kp66h69dyMHQwOSzuyIghXAUswuY24TZc0HdaJCXx2A=="
},
"node_modules/clone-deep": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
@ -9072,6 +9090,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/underscore": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz",
"integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g=="
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@ -11900,6 +11923,15 @@
"use-callback-ref": "^1.2.4"
}
},
"@silvermine/videojs-quality-selector": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@silvermine/videojs-quality-selector/-/videojs-quality-selector-1.2.5.tgz",
"integrity": "sha512-cielchUzL8r2EX01S7PfR54tTbxDZR53xIDJoUi9Wg6pM2X+ftdJD6XiIDVOjPlBoG94iuG9LJwUtjX5IhrWZQ==",
"requires": {
"class.extend": "0.9.1",
"underscore": "1.13.1"
}
},
"@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
@ -13056,6 +13088,11 @@
"resolved": "https://registry.npmjs.org/clamp/-/clamp-1.0.1.tgz",
"integrity": "sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA=="
},
"class.extend": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/class.extend/-/class.extend-0.9.1.tgz",
"integrity": "sha512-Tzj+2kAkZs+iGiUOUoKvtj4c/SjeVdKZXg/NbLTGKu0kp66h69dyMHQwOSzuyIghXAUswuY24TZc0HdaJCXx2A=="
},
"clone-deep": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
@ -17325,6 +17362,11 @@
"which-boxed-primitive": "^1.0.2"
}
},
"underscore": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz",
"integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g=="
},
"unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",

View File

@ -33,6 +33,7 @@
"@nextcloud/paths": "^2.1.0",
"@nextcloud/sharing": "^0.1.0",
"@nextcloud/vue": "7.0.0",
"@silvermine/videojs-quality-selector": "^1.2.5",
"camelcase": "^7.0.0",
"filerobot-image-editor": "^4.3.7",
"justified-layout": "^4.1.0",

View File

@ -1,6 +1,11 @@
import PhotoSwipe from "photoswipe";
import { generateUrl } from "@nextcloud/router";
import videojs from "video.js";
import "video.js/dist/video-js.min.css";
import qualitySelector from "@silvermine/videojs-quality-selector";
import "@silvermine/videojs-quality-selector/dist/css/quality-selector.css";
qualitySelector(videojs);
/**
* Check if slide has video content
@ -71,19 +76,53 @@ class VideoContentSetup {
e.preventDefault();
} else {
const content = <any>e.slide.content;
const fileid = content.data.photo.fileid;
// Create hls sources if enabled
let hlsSources = [];
const baseUrl = generateUrl(
`/apps/memories/api/video/transcode/${fileid}`
);
for (const q of ["360p", "480p", "720p", "1080p"]) {
hlsSources.push({
src: `${baseUrl}/${q}.m3u8`,
label: q,
type: "application/x-mpegURL",
selected: q === "480p" ? true : undefined,
});
}
content.videojs = videojs(content.videoElement, {
fluid: true,
autoplay: true,
controls: true,
sources: [{ src: e.slide.data.src }],
sources: [
...hlsSources,
{
src: e.slide.data.src,
label: "Original",
},
],
preload: "metadata",
inactivityTimeout: 0,
html5: {
vhs: {
withCredentials: true,
overrideNative: !videojs.browser.IS_SAFARI,
withCredentials: false,
},
},
controlBar: {
children: [
"playToggle",
"progressControl",
"volumePanel",
"qualitySelector",
"fullscreenToggle",
],
},
});
globalThis.videojs = content.videojs;
}
}
});