Skip to content
This repository has been archived by the owner on Jun 6, 2024. It is now read-only.

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
gorhill committed Jan 28, 2020
0 parents commit e4bf320
Show file tree
Hide file tree
Showing 14 changed files with 1,289 additions and 0 deletions.
18 changes: 18 additions & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"browser": true,
"devel": true,
"eqeqeq": true,
"esversion": 8,
"globals": {
"browser": false, // global variable in Firefox
"self": false
},
"laxbreak": true,
"newcap": false,
"nonew": false,
"strict": "global",
"sub": true,
"undef": true,
"unused": true,
"validthis": true
}
674 changes: 674 additions & 0 deletions LICENSE.txt

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## CCaptioner

A very simple extension which purpose is to assign a text track to a [HTML5
`video` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video)
in a web page.

Many HTML5 video players do not offer the ability to import text track for
captions/subtitles purpose. The purpose of this extension is to remediate
this problem.

When you want to assign a text track to a video element in a web page:

- Open the popup menu and click __"Assign text track to..."__
- Move the mouse over the target video element
- Click the video element if needed
- A file picker will appear
- Pick the `.srt` or `.vtt` file to use as text track

The video should now render the captions/subtitles of the file you
selected.

The content scripts of CCaptioner are injected **if and only if** you click on
its toolbar icon while on a specific web site, and only for that web site.
Once the text track is embedded, the content script terminates and should be
garbage-collected by your browser's JavaScript engine.

Once a text track has been assigned to a video element on a given page, you
can time-shift the text track through CCaptioner's popup panel -- this is
useful when the text track is not well synchronized with the video content.

### Permissions

#### `activeTab`

This permission means that the extension will be able to interact
with a web page **only** when you click its icon in the toolbar; so
CCaptioner's content script is injected **only** when you demand it by clicking
CCaptioner's toolbar icon.

#### `<all_urls>`

This permission is necessary to ensure CCaptioner's content script can also be
injected in embedded `iframe` elements in a page -- it is not uncommon for
video players to be inside an `iframe` which origin is different from the
origin of the root document.

### Credits

The CCaptioner's icon is from <https://en.wikipedia.org/wiki/File:Closed_captioning_symbol.svg>.
27 changes: 27 additions & 0 deletions make.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
#
# This script assumes a linux environment

echo "*** CCaptioner: Creating extension packages"

DES=~/Downloads/ccaptioner
rm -rf $DES

# Chromium
echo "*** Creating ccaptioner.chromium"
DESCH=$DES/ccaptioner.chromium
mkdir -p $DESCH
cp -R ./src/* $DESCH/
cp ./LICENSE.txt $DESCH/
cp ./manifest-chromium.json $DESCH/manifest.json

# Firefox
echo "*** Creating ccaptioner.firefox"
DESFF=$DES/ccaptioner.firefox
mkdir -p $DESFF
cp -R ./src/* $DESFF/
cp ./LICENSE.txt $DESFF/
cp ./manifest-firefox.json $DESFF/manifest.json
pushd $DESFF > /dev/null
zip ../$(basename $DESFF).xpi -qr *
popd > /dev/null
21 changes: 21 additions & 0 deletions manifest-chromium.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"author": "Raymond Hill",
"browser_action": {
"default_icon": {
"64": "icon-64.png"
},
"default_title": "CCaptioner",
"default_popup": "popup.html"
},
"description": "Assign your own text track to a video element in a web page",
"icons": {
"64": "icon-64.png"
},
"manifest_version": 2,
"name": "CCaptioner",
"permissions": [
"activeTab",
"<all_urls>"
],
"version": "1.0.0"
}
27 changes: 27 additions & 0 deletions manifest-firefox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"author": "Raymond Hill",
"browser_action": {
"default_icon": {
"64": "icon-64.png"
},
"default_title": "CCaptioner",
"default_popup": "popup.html"
},
"browser_specific_settings": {
"gecko": {
"id": "[email protected]",
"strict_min_version": "68.0"
}
},
"description": "Assign your own text track to a video element in a web page",
"icons": {
"64": "icon-64.png"
},
"manifest_version": 2,
"name": "CCaptioner",
"permissions": [
"activeTab",
"<all_urls>"
],
"version": "1.0.0"
}
144 changes: 144 additions & 0 deletions src/assign-captions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*******************************************************************************
CCaptioner - a browser extension to block requests.
Copyright (C) 2020-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/CCaptioner
*/

// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track

'use strict';

(( ) => {
if ( document.querySelector('video') === null ) { return; }
if (
self.closedCaptioner instanceof Object &&
self.closedCaptioner.closedCaptioner === true
) {
return;
}
self.closedCaptioner = { closedCaptioner: true };

let clientX = 0;
let clientY = 0;

const handleMouseEvent = function(ev) {
clientX = ev.clientX;
clientY = ev.clientY;
findVideoUnderMouse();
};
const listenerOptions = {
passive: true,
};

document.addEventListener('click', handleMouseEvent, listenerOptions);
document.addEventListener('mousedown', handleMouseEvent, listenerOptions);
document.addEventListener('mousemove', handleMouseEvent, listenerOptions);

let timer = setTimeout(
( ) => {
timer = undefined;
stop();
},
10000
);

const stop = function() {
document.removeEventListener('click', handleMouseEvent, listenerOptions);
document.removeEventListener('mousedown', handleMouseEvent, listenerOptions);
document.removeEventListener('mousemove', handleMouseEvent, listenerOptions);
if ( timer !== undefined ) {
clearTimeout(timer);
timer = undefined;
}
self.closedCaptioner = undefined;
};

const findVideoUnderMouse = function() {
const elems = document.elementsFromPoint(clientX, clientY);
for ( const elem of elems ) {
if ( elem.localName !== 'video' ) { continue; }
stop();
srtPick(elem);
}
};

const srtPick = function(video) {
const input = document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', '.srt,.vtt');
input.style.display = 'none';

input.addEventListener('change', ev => {
const button = ev.target;
const file = button.files[0];
if ( file === undefined || file.name === '' ) { return; }
if (
file.type.indexOf('text') !== 0 &&
file.type.indexOf('subrip') === -1
) {
return;
}
const fr = new FileReader();
fr.onload = ( ) => {
srtInstall(video, srtParse(fr.result));
};
fr.readAsText(file);
});

input.click();
video.pause();
};

const srtParse = function(raw) {
const vtt = [ 'WEBVTT', '' ];
const entries = raw.replace(/(\r\n|\n\r)/g, '\n')
.trim()
.split(/\s*\n\n+\s*/);
for ( const entry of entries ) {
const lines = entry.split(/\s*\n\s*/);
if ( /^\d+$/.test(lines[0]) === false ) { continue; }
const times = /(\S+)\s+--+>\s+(\S+)/.exec(lines[1]);
if ( times === null ) { continue; }
vtt.push(
lines[0],
times[1].replace(/,/g, '.') + ' --> ' + times[2].replace(/,/g, '.'),
lines.slice(2).join('\n'),
''
);
}
return vtt.join('\n');
};

const srtInstall = function(video, vtt) {
for ( const elem of video.querySelectorAll('track') ) {
elem.remove();
}
const blob = new Blob([ vtt ], { type: 'text/vtt' });
const blobURL = URL.createObjectURL(blob);
const track = document.createElement('track');
track.setAttribute('default', '');
track.setAttribute('kind', 'subtitles');
track.setAttribute('label', 'CCaptioner');
track.setAttribute('srclang', 'zz');
track.setAttribute('src', blobURL);
track.setAttribute('data-vtt-delta', '0');
track.setAttribute('data-vtt', vtt);
video.appendChild(track);
track.track.mode = 'showing';
};
})();
28 changes: 28 additions & 0 deletions src/get-time-offset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*******************************************************************************
CCaptioner - a browser extension to block requests.
Copyright (C) 2020-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/ccaptioner
*/

'use strict';

(( ) => {
const track = document.querySelector('video > track[label="CCaptioner"][data-vtt]');
if ( track === null ) { return; }
return parseFloat(track.getAttribute('data-vtt-offset') || '0') || 0;
})();
Binary file added src/icon-64.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 72 additions & 0 deletions src/offset-captions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*******************************************************************************
CCaptioner - a browser extension to block requests.
Copyright (C) 2020-present Raymond Hill
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see {http://www.gnu.org/licenses/}.
Home: https://github.com/gorhill/ccaptioner
*/

// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track

'use strict';

// Offset text track as per `data-vtt-offset` attribute

(( ) => {
const oldTrack = document.querySelector('video > track[label="CCaptioner"][data-vtt]');
if ( oldTrack === null ) { return; }

const timeDelta = parseInt(oldTrack.getAttribute('data-vtt-offset') || '0', 10);

let vtt = oldTrack.getAttribute('data-vtt');
if ( typeof vtt !== 'string' || vtt === '' ) { return; }

const timeShift = function(timecode) {
const fields = /(\d+):(\d+):(\d+).(\d+)/.exec(timecode);
let seconds = parseInt(fields[1], 10) * 3600 +
parseInt(fields[2], 10) * 60 +
parseInt(fields[3], 10) * 1 +
timeDelta;
if ( seconds < 0 ) { return '00:00:00.000'; }
const hh = Math.floor(seconds / 3600).toString().padStart(2, '0');
seconds %= 3600;
const mm = Math.floor(seconds / 60).toString().padStart(2, '0');
seconds %= 60;
const ss = seconds.toString().padStart(2, '0');
return `${hh}:${mm}:${ss}.${fields[4]}`;
};

const entries = vtt.trim().split(/\n\n/);

for ( let i = 0; i < entries.length; i++ ) {
const lines = entries[i].split(/\n/);
const times = /(\S+) --> (\S+)/.exec(lines[1]);
if ( times === null ) { continue; }
const t0 = timeShift(times[1]);
if ( t0 === '' ) { return; }
const t1 = timeShift(times[2]);
lines[1] = `${t0} --> ${t1}`;
entries[i] = lines.join('\n');
}
vtt = entries.join('\n\n');

const blob = new Blob([ vtt ], { type: 'text/vtt' });
const blobURL = URL.createObjectURL(blob);
const newTrack = oldTrack.cloneNode(false);
newTrack.setAttribute('src', blobURL);
oldTrack.replaceWith(newTrack);
newTrack.track.mode = 'showing';
})();
Loading

0 comments on commit e4bf320

Please sign in to comment.