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
This commit is contained in:
Feross Aboukhadijeh
2016-04-10 16:42:18 -07:00
parent f9141dd39c
commit 1a0a2b3658
6 changed files with 124 additions and 30 deletions

View File

@@ -1,4 +1,4 @@
**What version of WebTorrent Desktop?**
**What version of WebTorrent Desktop?** (See the 'About WebTorrent' menu)
**What operating system and version?**

View File

@@ -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",

View File

@@ -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
*/

View File

@@ -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()
}

View File

@@ -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
}
}
}

View File

@@ -54,6 +54,23 @@ function renderMedia (state) {
state.playing.volume = mediaElement.volume
}
// Add subtitles to the <video> tag
var trackTags = []
if (state.playing.subtitles.enabled && state.playing.subtitles.tracks.length > 0) {
for (var i = 0; i < state.playing.subtitles.tracks.length; i++) {
var track = state.playing.subtitles.tracks[i]
trackTags.push(hx`
<track
default=${track.selected ? 'default' : ''}
label=${track.language}
type='subtitles'
src=${track.buffer}>
`)
}
}
// Create the <audio> or <video> tag
var mediaTag = hx`
<div
@@ -66,9 +83,10 @@ function renderMedia (state) {
onstalling=${dispatcher('mediaStalled')}
ontimeupdate=${dispatcher('mediaTimeUpdate')}
autoplay>
${trackTags}
</div>
`
mediaTag.tagName = mediaType
mediaTag.tagName = mediaType // conditional tag name
// Show the media.
return hx`
@@ -236,6 +254,15 @@ function renderPlayerControls (state) {
`
]
// show closed captions icon
elements.push(hx`
<i.icon.closed-captions
class=${state.playing.subtitles.enabled ? 'active' : 'disabled'}
onclick=${handleSubtitles}>
closed_captions
</i>
`)
// If we've detected a Chromecast or AppleTV, the user can play video there
var isOnChromecast = state.playing.location.startsWith('chromecast')
var isOnAirplay = state.playing.location.startsWith('airplay')
@@ -385,6 +412,18 @@ function renderPlayerControls (state) {
break
}
}
function handleSubtitles (e) {
if (!state.playing.subtitles.tracks.length) {
// if no subtitles available select it
dispatch('openSubtitles')
} else {
// TODO: Show subtitles selector / disable
// dispatch('showSubtitlesMenu')
// meanwhile, just enable/disable
state.playing.subtitles.enabled = !state.playing.subtitles.enabled
}
}
}
// lets scrub without sending to volume backend