From 1a0a2b3658ee3b71c5c8cda5415463b60313520e Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Sun, 10 Apr 2016 16:42:18 -0700 Subject: [PATCH] Add subtitle support (via drag-n-drop) (#361) * issue template * cleanup closePlayer() and stopServer() * Add subtitle support (via drag-n-drop) Drag and drop a subtitles file (.srt or .vtt) onto the player (or the app icon on OS X) to add subtitles to the currently playing video. For #281 * add multiple subtitles structure * add open subtitle dialog from cc player controls --- .github/ISSUE_TEMPLATE.md | 2 +- package.json | 2 + renderer/index.css | 11 +++++ renderer/index.js | 92 +++++++++++++++++++++++++++------------ renderer/state.js | 6 ++- renderer/views/player.js | 41 ++++++++++++++++- 6 files changed, 124 insertions(+), 30 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 205e750e..87bf046f 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,4 @@ -**What version of WebTorrent Desktop?** +**What version of WebTorrent Desktop?** (See the 'About WebTorrent' menu) **What operating system and version?** diff --git a/package.json b/package.json index 17e2c89a..0b066896 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "application-config-path": "^0.1.0", "bitfield": "^1.0.2", "chromecasts": "^1.8.0", + "concat-stream": "^1.5.1", "create-torrent": "^3.22.1", "deep-equal": "^1.0.1", "dlnacasts": "^0.0.3", @@ -34,6 +35,7 @@ "prettier-bytes": "^1.0.1", "rimraf": "^2.5.2", "simple-get": "^2.0.0", + "srt-to-vtt": "^1.0.3", "upload-element": "^1.0.1", "virtual-dom": "^2.1.1", "webtorrent": "^0.90.0", diff --git a/renderer/index.css b/renderer/index.css index 199944f5..9e0d9016 100644 --- a/renderer/index.css +++ b/renderer/index.css @@ -677,6 +677,7 @@ body.drag .app::after { .player-controls .device, .player-controls .fullscreen, +.player-controls .closed-captions, .player-controls .volume-icon, .player-controls .back { display: block; @@ -691,6 +692,7 @@ body.drag .app::after { } .player-controls .device, +.player-controls .closed-captions, .player-controls .fullscreen { float: right; } @@ -752,6 +754,15 @@ body.drag .app::after { height: 14px; } +::cue { + background: none; + color: #FFF; + font: 24px; + line-height: 1.3em; + text-shadow: #000 -1px 0 1px, #000 1px 0 1px, #000 0 -1px 1px, #000 0 1px 1px, rgba(50, 50, 50, 0.5) 2px 2px 0; +} + + /* * CHROMECAST / AIRPLAY CONTROLS */ diff --git a/renderer/index.js b/renderer/index.js index dd510906..64105c98 100644 --- a/renderer/index.js +++ b/renderer/index.js @@ -1,6 +1,7 @@ console.time('init') var cfg = require('application-config')('WebTorrent') +var concat = require('concat-stream') var dragDrop = require('drag-drop') var electron = require('electron') var EventEmitter = require('events') @@ -8,6 +9,7 @@ var fs = require('fs') var mainLoop = require('main-loop') var path = require('path') var remote = require('remote') +var srtToVtt = require('srt-to-vtt') var createElement = require('virtual-dom/create-element') var diff = require('virtual-dom/diff') @@ -255,6 +257,9 @@ function dispatch (action, ...args) { if (action === 'setVolume') { setVolume(args[0] /* increase */) } + if (action === 'openSubtitles') { + openSubtitles() + } if (action === 'mediaPlaying') { state.playing.isPaused = false ipcRenderer.send('blockPowerSave') @@ -331,7 +336,6 @@ function changeVolume (delta) { setVolume(state.playing.volume + delta) } -// TODO: never called. Either remove or make a volume control that calls it function setVolume (volume) { // check if its in [0.0 - 1.0] range volume = Math.max(0, Math.min(1, volume)) @@ -342,6 +346,17 @@ function setVolume (volume) { } } +function openSubtitles () { + dialog.showOpenDialog({ + title: 'Select a subtitles file.', + filters: [ { name: 'Subtitles', extensions: ['vtt', 'srt'] } ], + properties: [ 'openFile' ] + }, function (filenames) { + if (!Array.isArray(filenames)) return + addSubtitle({path: filenames[0]}) + }) +} + // Checks whether we are connected and already casting // Returns false if we not casting (state.playing.location === 'local') // or if we're trying to connect but haven't yet ('chromecast-pending', etc) @@ -459,12 +474,35 @@ function onOpen (files) { if (!Array.isArray(files)) files = [ files ] // .torrent file = start downloading the torrent - files.filter(isTorrent).forEach(function (torrentFile) { - addTorrent(torrentFile) - }) + files.filter(isTorrent).forEach(addTorrent) + + // subtitle file + files.filter(isSubtitle).forEach(addSubtitle) // everything else = seed these files - createTorrentFromFileObjects(files.filter(isNotTorrent)) + var rest = files.filter(not(isTorrent)).filter(not(isSubtitle)) + if (rest.length > 0) { + createTorrentFromFileObjects(rest) + } +} + +function isTorrent (file) { + var name = typeof file === 'string' ? file : file.name + var isTorrentFile = path.extname(name).toLowerCase() === '.torrent' + var isMagnet = typeof file === 'string' && /^magnet:/.test(file) + return isTorrentFile || isMagnet +} + +function isSubtitle (file) { + var name = typeof file === 'string' ? file : file.name + var ext = path.extname(name).toLowerCase() + return ext === '.srt' || ext === '.vtt' +} + +function not (test) { + return function (...args) { + return !test(...args) + } } function onPaste (e) { @@ -478,17 +516,6 @@ function onPaste (e) { }) } -function isTorrent (file) { - var name = typeof file === 'string' ? file : file.name - var isTorrentFile = path.extname(name).toLowerCase() === '.torrent' - var isMagnet = typeof file === 'string' && /^magnet:/.test(file) - return isTorrentFile || isMagnet -} - -function isNotTorrent (file) { - return !isTorrent(file) -} - // Gets a torrent summary {name, infoHash, status} from state.saved.torrents // Returns undefined if we don't know that infoHash function getTorrentSummary (torrentKey) { @@ -509,6 +536,23 @@ function addTorrent (torrentId) { ipcRenderer.send('wt-start-torrenting', torrentKey, torrentId, path) } +function addSubtitle (file) { + if (state.playing.type !== 'video') return + fs.createReadStream(file.path || file).pipe(srtToVtt()).pipe(concat(function (buf) { + // Set the cue text position so it appears above the player controls. + // The only way to change cue text position is by modifying the VTT. It is not + // possible via CSS. + var subtitles = Buffer(buf.toString().replace(/(-->.*)/g, '$1 line:88%')) + var track = { + buffer: 'data:text/vtt;base64,' + subtitles.toString('base64'), + language: 'Language ' + state.playing.subtitles.tracks.length, + selected: true + } + state.playing.subtitles.tracks.push(track) + state.playing.subtitles.enabled = true + })) +} + // Starts downloading and/or seeding a given torrentSummary. Returns WebTorrent object function startTorrentingSummary (torrentSummary) { var s = torrentSummary @@ -744,12 +788,6 @@ function pickFileToPlay (files) { return undefined } -function stopServer () { - ipcRenderer.send('wt-stop-server') - state.playing = State.getDefaultPlayState() - state.server = null -} - // Opens the video player function openPlayer (infoHash, index, cb) { var torrentSummary = getTorrentSummary(infoHash) @@ -817,23 +855,23 @@ function openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) { } function closePlayer (cb) { - state.window.title = config.APP_WINDOW_TITLE - update() /* needed for OSX: toggleFullScreen animation w/ correct title */ - if (isCasting()) { Cast.close() } + state.window.title = config.APP_WINDOW_TITLE + state.playing = State.getDefaultPlayState() + state.server = null if (state.window.isFullScreen) { dispatch('toggleFullScreen', false) } restoreBounds() - stopServer() - update() + ipcRenderer.send('wt-stop-server') ipcRenderer.send('unblockPowerSave') ipcRenderer.send('onPlayerClose') + update() cb() } diff --git a/renderer/state.js b/renderer/state.js index e577fd07..08e8b326 100644 --- a/renderer/state.js +++ b/renderer/state.js @@ -70,7 +70,11 @@ function getDefaultPlayState () { isPaused: true, isStalled: false, lastTimeUpdate: 0, /* Unix time in ms */ - mouseStationarySince: 0 /* Unix time in ms */ + mouseStationarySince: 0, /* Unix time in ms */ + subtitles: { + tracks: [], /* subtitles file (Buffer) */ + enabled: false + } } } diff --git a/renderer/views/player.js b/renderer/views/player.js index 85d54945..4d937392 100644 --- a/renderer/views/player.js +++ b/renderer/views/player.js @@ -54,6 +54,23 @@ function renderMedia (state) { state.playing.volume = mediaElement.volume } + // Add subtitles to the