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:
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,4 +1,4 @@
|
||||
**What version of WebTorrent Desktop?**
|
||||
**What version of WebTorrent Desktop?** (See the 'About WebTorrent' menu)
|
||||
|
||||
**What operating system and version?**
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user