Skip to content

Commit

Permalink
v2.2.7: Pre-rendered server-side thumbnails
Browse files Browse the repository at this point in the history
* Added ability to use pre-rendered server-side thumbnail files
* Dropped EdgeHTML support
* Updated to folder.api v1.0.3 - minor date metadata bug
  • Loading branch information
pseudosavant committed Apr 8, 2022
1 parent dc9ba9c commit 5b0a769
Show file tree
Hide file tree
Showing 12 changed files with 103 additions and 50 deletions.
32 changes: 25 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ To use it, copy the [`./src/player.html`](src/player.html) file into a folder th
* Custom video playback controls (fullscreen, play, pause, mute, etc, volume, playback rate)
* Picture-in-picture support
* Progress bar with timestamp preview thumbnail on hover
* Video thumbnail generation, with concurrency configuration (default 1)*
* Video thumbnails: [using prerendered thumbnail files](#thumbnails), or rendered on-the-fly in-browser
* Animated thumbnails**
* Thumbnail caching using `localStorage`, check cache size, clear cache
* Select your own custom theme color
Expand All @@ -34,19 +34,37 @@ To use it, copy the [`./src/player.html`](src/player.html) file into a folder th

\** Animated thumbnails can consume a lot of data. The experience may degrade on slower network connections

## Supported Browsers
<a name="thumbnails"></a>
## Pre-rendered server-side thumbnails

`player.html` can use server-side thumbnails for any video that has one available. It will fall back to generating thumbnails in the browser otherwise. The server-side thumbnail files must follow the common filename convention of using the video file name, with the extension replaced with an image extension, in the same folder as the video. Note: The image files must be shown by your web server's directory browsing (may require mime-type adjustments on some servers) feature to show up in `player.html`.

### Naming example:

* Video filename: `myVideo.mp4`
* Matching thumbnail filename: `myVideo.jpg`

### How to pre-render thumbnails

The easiest way to create thumbnails from video files is on the command-line with [`ffmpeg`](https://ffmpeg.org/). The following command will create a JPEG thumbnail (`myVideo.jpg`) from the video frame 5 seconds into `myVideo.mp4`: `ffmpeg -i myVideo.mp4 -ss 00:00:05.000 -vframes 1 myVideo.jpg`. Loop over folders of video files using your preferred shell (bash, cmd, powershell, etc) to process many videos.

### Supported thumbnail image formats/extensions
* GIF
* JPEG
* JPG
* PNG
* WEBP

## Supported browsers

The latest version of these browsers is supported:

* Edge (Chromium)
* Edge (Xbox EdgeHTML)†
* Firefox
* Safari (Mac, iPadOS, iOS)
* Chrome

† Deprecated: EdgeHTML is only supported on Xbox as it has been replaced on Windows 10.

## Supported Web Servers
## Supported web servers

The latest version of these web servers (others may work as well):

Expand All @@ -63,4 +81,4 @@ The latest version of these web servers (others may work as well):

* [MIT](./LICENSE)

&copy; 2021 Paul Ellis
&copy; 2022 Paul Ellis
121 changes: 78 additions & 43 deletions src/player.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<title>player.html</title>
<meta charset='utf-8'>
<meta name='description' content='Drop player.html in a folder of videos and play them on the web' />
<meta name='version' content='2.2.6'>
<meta name='version' content='2.2.7'>
<meta name='Copyright' content='©2021 Paul Ellis'>
<meta name='license' content='https://opensource.org/licenses/MIT'>
<meta name='apple-mobile-web-app-status-bar-style' content='black'>
Expand Down Expand Up @@ -2036,11 +2036,11 @@

<!-- folder.api -->
<script defer>
// folder.api (https://github.com/pseudosavant/folder.api): v1.0.2
(function folderApiIIFE(global) {
'use strict';
// folder.api: version 1.0.3
(function folderApiIIFE(global) {
'use strict';

function urlType(url) {
const urlType = (url) => {
if (isHiddenFileOrFolder(url)) {
return 'hidden';
} else if (isFolder(url)) {
Expand All @@ -2052,7 +2052,7 @@
}
}

function isFile(url) {
const isFile = (url) => {
const parsed = new URL(url);
const finalPosition = parsed.pathname.lastIndexOf('/') + 1;
const finalPart = parsed.pathname.substring(finalPosition);
Expand All @@ -2063,39 +2063,39 @@
const hasPeriod = (s) => s.indexOf('.') >= 0;
const isFolder = (url) => !isFile(url);

function isHiddenFileOrFolder(url) {
const isHiddenFileOrFolder = (url) => {
const reHidden = /\/\..+$/i;
return url.toString().match(reHidden);
}

function parentFolder(url) {
const parentFolder = (url) => {
// Only add the first slash if the URL doesn't have it at the end
const append = (url.endsWith('/') ? '../' : '/../');
const parentUrl = new URL(url + append).toString();
return parentUrl;
}

function urlToFoldername(url) {
const urlToFoldername = (url) => {
var pieces = url.split('/');
return pieces[pieces.length - 2]; // Return piece before final `/`
}

function urlToFilename(url) {
const urlToFilename = (url) => {
const re = /\/([^/]+)$/;
const parts = re.exec(url);
return (parts && parts.length > 1 ? parts[1] : url);
}

// Parses each node's metadata by running a function on each entry
async function linkToMetadata(node, server) {
const linkToMetadata = async (node, server) => {
return (
typeof servers[server] === 'function' ?
servers[server](node) :
{}
);
}

async function getHeaderData(url) {
const getHeaderData = async (url) => {
if (!url) return {};

try {
Expand All @@ -2108,7 +2108,7 @@
}
}

async function getServer(url) {
const getServer = async (url) => {
const headers = await getHeaderData(url);

if (isNginx(headers)) {
Expand Down Expand Up @@ -2154,11 +2154,11 @@
const dateResults = dateRe.exec(dateNode.textContent);

if (dateResults) {
const y = toInteger(dateResults[1]) || undefined;
const m = toInteger(dateResults[2]) || undefined;
const d = toInteger(dateResults[3]) || undefined;
const hours = toInteger(dateResults[4]) || undefined;
const mins = toInteger(dateResults[5]) || undefined;
const d = (toInteger(dateResults[1]) >= 0 ? toInteger(dateResults[1]) : undefined);
const m = (isString(dateResults[2]) ? dateResults[2] : undefined);
const y = (toInteger(dateResults[3]) >= 0 ? toInteger(dateResults[3]) : undefined);
const hours = (toInteger(dateResults[4]) >= 0 ? toInteger(dateResults[4]) : undefined);
const mins = (toInteger(dateResults[5]) >= 0 ? toInteger(dateResults[5]) : undefined);
metadata.date = new Date(`${m}-${d}, ${y} ${hours}:${mins}:00`);
}
}
Expand Down Expand Up @@ -2196,14 +2196,14 @@
const results = re.exec(text);

if (!results) return metadata;
const d = toInteger(results[1]) || undefined;
const m = results[2] || undefined;
const y = toInteger(results[3]) || undefined;
const hours = toInteger(results[4]) || undefined;
const mins = toInteger(results[5]) || undefined;
const d = (toInteger(results[1]) >= 0 ? toInteger(results[1]) : undefined);
const m = (isString(results[2]) ? results[2] : undefined);
const y = (toInteger(results[3]) >= 0 ? toInteger(results[3]) : undefined);
const hours = (toInteger(results[4]) >= 0 ? toInteger(results[4]) : undefined);
const mins = (toInteger(results[5]) >= 0 ? toInteger(results[5]) : undefined);
metadata.date = new Date(`${m}-${d}, ${y} ${hours}:${mins}:00`);

metadata.size = toInteger(results[6]) || undefined;
metadata.size = (isNumber(toInteger(results[6])) ? toInteger(results[6]) : undefined);

return metadata;
},
Expand All @@ -2218,14 +2218,14 @@
const results = re.exec(text);
if (!results) return metadata;

const m = toInteger(results[1]) || undefined;
const d = toInteger(results[2]) || undefined;
const y = toInteger(results[3]) || undefined;
const hours = toInteger(results[4]) || undefined;
const mins = toInteger(results[5]);
const d = (toInteger(results[1]) >= 0 ? toInteger(results[1]) : undefined);
const m = (isString(results[2]) ? results[2] : undefined);
const y = (toInteger(results[3]) >= 0 ? toInteger(results[3]) : undefined);
const hours = (toInteger(results[4]) >= 0 ? toInteger(results[4]) : undefined);
const mins = (toInteger(results[5]) >= 0 ? toInteger(results[5]) : undefined);
metadata.date = new Date(`${m}-${d}, ${y} ${hours}:${mins}:00`);

metadata.size = toInteger(results[7]) || undefined;
metadata.size = (isNumber(toInteger(results[7])) ? toInteger(results[7]) : undefined);

return metadata;
},
Expand Down Expand Up @@ -2265,20 +2265,20 @@
const results = re.exec(text);

if (!results) return metadata;
const d = toInteger(results[1]) || undefined;
const m = results[2] || undefined;
const y = toInteger(results[3]) || undefined;
const hours = toInteger(results[4]) || undefined;
const mins = toInteger(results[5]) || undefined;
const d = (toInteger(results[1]) >= 0 ? toInteger(results[1]) : undefined);
const m = (isString(results[2]) ? results[2] : undefined);
const y = (toInteger(results[3]) >= 0 ? toInteger(results[3]) : undefined);
const hours = (toInteger(results[4]) >= 0 ? toInteger(results[4]) : undefined);
const mins = (toInteger(results[5]) >= 0 ? toInteger(results[5]) : undefined);
metadata.date = new Date(`${m}-${d}, ${y} ${hours}:${mins}:00`);

metadata.size = toInteger(results[6]) || undefined;
metadata.size = (isNumber(toInteger(results[6])) ? toInteger(results[6]) : undefined);

return metadata;
},
}

async function getLinksFromFrame(frame, baseUrl) {
const getLinksFromFrame = async (frame, baseUrl) => {
const server = await getServer(baseUrl) || 'generic';

var query;
Expand Down Expand Up @@ -2356,7 +2356,7 @@
return { server, folders, files };
}

async function folderApiRequest(url) {
const folderApiRequest = async (url) => {
const $frame = document.createElement('iframe');
$frame.style.visibility = 'hidden';
$frame.style.overflow = 'hidden';
Expand All @@ -2381,7 +2381,10 @@

const toNumber = (d) => +d;
const toInteger = (d) => parseInt(d, 10);
const isUndefined = (v) => typeof v === 'undefined';
const is = (type) => (v) => typeof v === type;
const isNumber = is('number');
const isString = is('string');
const isUndefined = is('undefined');

global.folderApiRequest = folderApiRequest;
})(this);
Expand Down Expand Up @@ -2543,6 +2546,11 @@
return re.test(haystack);
};

const isImage = (url) => {
const re = /(?:\/)((?:[^/])+\.(?:png|jpeg|jpg|gif|webp))/gi
return re.test(url);
}

const isHiddenFileOrFolder = (url) => {
const reHidden = /\/\..+$/i;
return url.toString().match(reHidden);
Expand Down Expand Up @@ -2589,6 +2597,28 @@
html += createFolderTemplate(rawUrl, label);
});

const removeUrlExtension = (url) => {
const re = /^(.*)(?:\.\w+)$/i;
const results = re.exec(url);

if (results.length > 0) return results[1];

return undefined;
}

const findThumbnail = (videoUrl, files) => {
if (!Array.isArray(files)) return undefined;

const videoPrefix = removeUrlExtension(videoUrl);

const thumbnails = files.filter((file) => isImage(file.url));
const thumb = thumbnails.find((file) => {
const filePrefix = removeUrlExtension(file.url);
return videoPrefix === filePrefix;
});
return thumb;
}

const subtitles = files.filter((file) => isSubtitle(file.url));
populateSubtitles(subtitles);

Expand All @@ -2601,8 +2631,10 @@
const url = decodeURI(rawUrl).replace(base, '');
const label = urlToLabel(url);
const cssClasses = (rawUrl === $player.src ? 'current' : '');

html += createFileTemplate(rawUrl, label, cssClasses);
const preRenderedThumbnail = findThumbnail(file.url, links.files);
const thumbnailUrl = (preRenderedThumbnail && preRenderedThumbnail.url ? preRenderedThumbnail.url : '');

html += createFileTemplate(rawUrl, label, cssClasses, thumbnailUrl);
});

$('.links').innerHTML = html;
Expand Down Expand Up @@ -2637,8 +2669,8 @@
updateHash();
}

const createFileTemplate = (url, label, optionalClasses = '') => {
return `<a href='${url}' class='file ${optionalClasses}' draggable='false'>
const createFileTemplate = (url, label, optionalClasses = '', preRenderedThumbnailUrl = '' ) => {
return `<a href='${url}' class='file ${optionalClasses}' style='${(preRenderedThumbnailUrl ? '--image-url-0: url(' + preRenderedThumbnailUrl + ')': '')}' draggable='false'>
<div class='title' draggable='false'>${label}</div>
<div class='arrow' draggable='false'>
<svg><use xlink:href='#svg-play'/></svg>
Expand Down Expand Up @@ -3752,7 +3784,10 @@
!$player.ended
);

const hasPreRenderedThumbnail = (node) => node.style.getPropertyValue('--image-url-0').length > ('url(http://)').length;

const setThumbnail = async (node, url) => {
if (hasPreRenderedThumbnail(node)) return;
const thumbnailOpts = app.options.thumbnails;

const timestamps = (retrieveSetting('animate') ? [...thumbnailOpts.timestamps] : [thumbnailOpts.timestamps[0]]);
Expand Down
Binary file added videos/prerendered-thumbnail-gif.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
Binary file added videos/prerendered-thumbnail-jpeg.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
Binary file added videos/prerendered-thumbnail-jpg.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
Empty file.
Binary file added videos/prerendered-thumbnail-png.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
Binary file added videos/prerendered-thumbnail-webp.webp
Binary file not shown.

0 comments on commit 5b0a769

Please sign in to comment.