Add playlists feature (#871)
* Open multi-file torrents as playlists
* Add `repeat` and `shuffle` options to the player
* Autoplay first file in torrent
* replaces `pickFileToPlay` feature
* when reopening player, restores the most recently viewed file
* Add playlist navigation buttons to Windows thumbar
* Remove `repeat` and `shuffle` options
This reverts commit 9284122461.
* Play files in order they appear in torrent
* Clean up playlists code
This commit is contained in:
@@ -64,6 +64,11 @@ function init () {
|
|||||||
thumbar.enable()
|
thumbar.enable()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipc.on('onPlayerUpdate', function (e, ...args) {
|
||||||
|
menu.onPlayerUpdate(...args)
|
||||||
|
thumbar.onPlayerUpdate(...args)
|
||||||
|
})
|
||||||
|
|
||||||
ipc.on('onPlayerClose', function () {
|
ipc.on('onPlayerClose', function () {
|
||||||
menu.setPlayerOpen(false)
|
menu.setPlayerOpen(false)
|
||||||
powerSaveBlocker.disable()
|
powerSaveBlocker.disable()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ module.exports = {
|
|||||||
setPlayerOpen,
|
setPlayerOpen,
|
||||||
setWindowFocus,
|
setWindowFocus,
|
||||||
setAllowNav,
|
setAllowNav,
|
||||||
|
onPlayerUpdate,
|
||||||
onToggleAlwaysOnTop,
|
onToggleAlwaysOnTop,
|
||||||
onToggleFullScreen
|
onToggleFullScreen
|
||||||
}
|
}
|
||||||
@@ -25,6 +26,8 @@ function init () {
|
|||||||
|
|
||||||
function setPlayerOpen (flag) {
|
function setPlayerOpen (flag) {
|
||||||
getMenuItem('Play/Pause').enabled = flag
|
getMenuItem('Play/Pause').enabled = flag
|
||||||
|
getMenuItem('Skip Next').enabled = flag
|
||||||
|
getMenuItem('Skip Previous').enabled = flag
|
||||||
getMenuItem('Increase Volume').enabled = flag
|
getMenuItem('Increase Volume').enabled = flag
|
||||||
getMenuItem('Decrease Volume').enabled = flag
|
getMenuItem('Decrease Volume').enabled = flag
|
||||||
getMenuItem('Step Forward').enabled = flag
|
getMenuItem('Step Forward').enabled = flag
|
||||||
@@ -32,6 +35,16 @@ function setPlayerOpen (flag) {
|
|||||||
getMenuItem('Increase Speed').enabled = flag
|
getMenuItem('Increase Speed').enabled = flag
|
||||||
getMenuItem('Decrease Speed').enabled = flag
|
getMenuItem('Decrease Speed').enabled = flag
|
||||||
getMenuItem('Add Subtitles File...').enabled = flag
|
getMenuItem('Add Subtitles File...').enabled = flag
|
||||||
|
|
||||||
|
if (flag === false) {
|
||||||
|
getMenuItem('Skip Next').enabled = false
|
||||||
|
getMenuItem('Skip Previous').enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlayerUpdate (hasNext, hasPrevious) {
|
||||||
|
getMenuItem('Skip Next').enabled = hasNext
|
||||||
|
getMenuItem('Skip Previous').enabled = hasPrevious
|
||||||
}
|
}
|
||||||
|
|
||||||
function setWindowFocus (flag) {
|
function setWindowFocus (flag) {
|
||||||
@@ -187,6 +200,21 @@ function getMenuTemplate () {
|
|||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Skip Next',
|
||||||
|
accelerator: 'N',
|
||||||
|
click: () => windows.main.dispatch('nextTrack'),
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Skip Previous',
|
||||||
|
accelerator: 'P',
|
||||||
|
click: () => windows.main.dispatch('previousTrack'),
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Increase Volume',
|
label: 'Increase Volume',
|
||||||
accelerator: 'CmdOrCtrl+Up',
|
accelerator: 'CmdOrCtrl+Up',
|
||||||
|
|||||||
@@ -12,9 +12,19 @@ function enable () {
|
|||||||
'MediaPlayPause',
|
'MediaPlayPause',
|
||||||
() => windows.main.dispatch('playPause')
|
() => windows.main.dispatch('playPause')
|
||||||
)
|
)
|
||||||
|
electron.globalShortcut.register(
|
||||||
|
'MediaNextTrack',
|
||||||
|
() => windows.main.dispatch('nextTrack')
|
||||||
|
)
|
||||||
|
electron.globalShortcut.register(
|
||||||
|
'MediaPreviousTrack',
|
||||||
|
() => windows.main.dispatch('previousTrack')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function disable () {
|
function disable () {
|
||||||
// Return the media key to the OS, so other apps can use it.
|
// Return the media key to the OS, so other apps can use it.
|
||||||
electron.globalShortcut.unregister('MediaPlayPause')
|
electron.globalShortcut.unregister('MediaPlayPause')
|
||||||
|
electron.globalShortcut.unregister('MediaNextTrack')
|
||||||
|
electron.globalShortcut.unregister('MediaPreviousTrack')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ module.exports = {
|
|||||||
disable,
|
disable,
|
||||||
enable,
|
enable,
|
||||||
onPlayerPause,
|
onPlayerPause,
|
||||||
onPlayerPlay
|
onPlayerPlay,
|
||||||
|
onPlayerUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,39 +17,75 @@ var config = require('../config')
|
|||||||
|
|
||||||
var windows = require('./windows')
|
var windows = require('./windows')
|
||||||
|
|
||||||
|
const PREV_ICON = path.join(config.STATIC_PATH, 'PreviousTrackThumbnailBarButton.png')
|
||||||
|
const PLAY_ICON = path.join(config.STATIC_PATH, 'PlayThumbnailBarButton.png')
|
||||||
|
const PAUSE_ICON = path.join(config.STATIC_PATH, 'PauseThumbnailBarButton.png')
|
||||||
|
const NEXT_ICON = path.join(config.STATIC_PATH, 'NextTrackThumbnailBarButton.png')
|
||||||
|
|
||||||
|
// Array indices for each button
|
||||||
|
const PREV = 0
|
||||||
|
const PLAY_PAUSE = 1
|
||||||
|
const NEXT = 2
|
||||||
|
|
||||||
|
var buttons = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the Windows thumbnail toolbar buttons.
|
* Show the Windows thumbnail toolbar buttons.
|
||||||
*/
|
*/
|
||||||
function enable () {
|
function enable () {
|
||||||
update(false)
|
buttons = [
|
||||||
|
{
|
||||||
|
tooltip: 'Previous Track',
|
||||||
|
icon: PREV_ICON,
|
||||||
|
click: () => windows.main.dispatch('previousTrack')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: 'Pause',
|
||||||
|
icon: PAUSE_ICON,
|
||||||
|
click: () => windows.main.dispatch('playPause')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: 'Next Track',
|
||||||
|
icon: NEXT_ICON,
|
||||||
|
click: () => windows.main.dispatch('nextTrack')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide the Windows thumbnail toolbar buttons.
|
* Hide the Windows thumbnail toolbar buttons.
|
||||||
*/
|
*/
|
||||||
function disable () {
|
function disable () {
|
||||||
windows.main.win.setThumbarButtons([])
|
buttons = []
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerPause () {
|
function onPlayerPause () {
|
||||||
update(true)
|
if (!isEnabled()) return
|
||||||
|
buttons[PLAY_PAUSE].tooltip = 'Play'
|
||||||
|
buttons[PLAY_PAUSE].icon = PLAY_ICON
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPlayerPlay () {
|
function onPlayerPlay () {
|
||||||
update(false)
|
if (!isEnabled()) return
|
||||||
|
buttons[PLAY_PAUSE].tooltip = 'Pause'
|
||||||
|
buttons[PLAY_PAUSE].icon = PAUSE_ICON
|
||||||
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function update (isPaused) {
|
function onPlayerUpdate (state) {
|
||||||
var icon = isPaused
|
if (!isEnabled()) return
|
||||||
? 'PlayThumbnailBarButton.png'
|
buttons[PREV].flags = [ state.hasPrevious ? 'enabled' : 'disabled' ]
|
||||||
: 'PauseThumbnailBarButton.png'
|
buttons[NEXT].flags = [ state.hasNext ? 'enabled' : 'disabled' ]
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
var buttons = [
|
function isEnabled () {
|
||||||
{
|
return buttons.length > 0
|
||||||
tooltip: isPaused ? 'Play' : 'Pause',
|
}
|
||||||
icon: path.join(config.STATIC_PATH, icon),
|
|
||||||
click: () => windows.main.dispatch('playPause')
|
function update () {
|
||||||
}
|
|
||||||
]
|
|
||||||
windows.main.win.setThumbarButtons(buttons)
|
windows.main.win.setThumbarButtons(buttons)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ const electron = require('electron')
|
|||||||
|
|
||||||
const ipcRenderer = electron.ipcRenderer
|
const ipcRenderer = electron.ipcRenderer
|
||||||
|
|
||||||
|
const Playlist = require('../lib/playlist')
|
||||||
|
|
||||||
// Controls local play back: the <video>/<audio> tag and VLC
|
// Controls local play back: the <video>/<audio> tag and VLC
|
||||||
// Does not control remote casting (Chromecast etc)
|
// Does not control remote casting (Chromecast etc)
|
||||||
module.exports = class MediaController {
|
module.exports = class MediaController {
|
||||||
@@ -44,7 +46,8 @@ module.exports = class MediaController {
|
|||||||
|
|
||||||
openExternalPlayer () {
|
openExternalPlayer () {
|
||||||
var state = this.state
|
var state = this.state
|
||||||
ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, state.server.localURL, state.window.title)
|
var mediaURL = Playlist.getCurrentLocalURL(this.state)
|
||||||
|
ipcRenderer.send('openExternalPlayer', state.saved.prefs.externalPlayerPath, mediaURL, state.window.title)
|
||||||
state.playing.location = 'external'
|
state.playing.location = 'external'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const errors = require('../lib/errors')
|
|||||||
const sound = require('../lib/sound')
|
const sound = require('../lib/sound')
|
||||||
const TorrentPlayer = require('../lib/torrent-player')
|
const TorrentPlayer = require('../lib/torrent-player')
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
const Playlist = require('../lib/playlist')
|
||||||
const State = require('../lib/state')
|
const State = require('../lib/state')
|
||||||
|
|
||||||
const ipcRenderer = electron.ipcRenderer
|
const ipcRenderer = electron.ipcRenderer
|
||||||
@@ -24,18 +25,29 @@ module.exports = class PlaybackController {
|
|||||||
// Play a file in a torrent.
|
// Play a file in a torrent.
|
||||||
// * Start torrenting, if necessary
|
// * Start torrenting, if necessary
|
||||||
// * Stream, if not already fully downloaded
|
// * Stream, if not already fully downloaded
|
||||||
// * If no file index is provided, pick the default file to play
|
// * If no file index is provided, restore the most recently viewed file or autoplay the first
|
||||||
playFile (infoHash, index /* optional */) {
|
playFile (infoHash, index /* optional */) {
|
||||||
this.state.location.go({
|
var state = this.state
|
||||||
url: 'player',
|
if (state.location.url() === 'player') {
|
||||||
setup: (cb) => {
|
this.updatePlayer(infoHash, index, false, (err) => {
|
||||||
this.play()
|
if (err) dispatch('error', err)
|
||||||
this.openPlayer(infoHash, index, cb)
|
else this.play()
|
||||||
},
|
})
|
||||||
destroy: () => this.closePlayer()
|
} else {
|
||||||
}, (err) => {
|
state.location.go({
|
||||||
if (err) dispatch('error', err)
|
url: 'player',
|
||||||
})
|
setup: (cb) => {
|
||||||
|
this.play()
|
||||||
|
this.openPlayer(infoHash, index, (err) => {
|
||||||
|
if (!err) this.play
|
||||||
|
cb(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
destroy: () => this.closePlayer()
|
||||||
|
}, (err) => {
|
||||||
|
if (err) dispatch('error', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open a file in OS default app.
|
// Open a file in OS default app.
|
||||||
@@ -64,6 +76,30 @@ module.exports = class PlaybackController {
|
|||||||
else this.pause()
|
else this.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Play next file in list (if any)
|
||||||
|
nextTrack () {
|
||||||
|
var state = this.state
|
||||||
|
if (Playlist.hasNext(state)) {
|
||||||
|
this.updatePlayer(
|
||||||
|
state.playing.infoHash, Playlist.getNextIndex(state), false, (err) => {
|
||||||
|
if (err) dispatch('error', err)
|
||||||
|
else this.play()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play previous track in list (if any)
|
||||||
|
previousTrack () {
|
||||||
|
var state = this.state
|
||||||
|
if (Playlist.hasPrevious(state)) {
|
||||||
|
this.updatePlayer(
|
||||||
|
state.playing.infoHash, Playlist.getPreviousIndex(state), false, (err) => {
|
||||||
|
if (err) dispatch('error', err)
|
||||||
|
else this.play()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Play (unpause) the current media
|
// Play (unpause) the current media
|
||||||
play () {
|
play () {
|
||||||
var state = this.state
|
var state = this.state
|
||||||
@@ -169,13 +205,16 @@ module.exports = class PlaybackController {
|
|||||||
|
|
||||||
// Opens the video player to a specific torrent
|
// Opens the video player to a specific torrent
|
||||||
openPlayer (infoHash, index, cb) {
|
openPlayer (infoHash, index, cb) {
|
||||||
var torrentSummary = TorrentSummary.getByKey(this.state, infoHash)
|
var state = this.state
|
||||||
|
|
||||||
// automatically choose which file in the torrent to play, if necessary
|
var torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
||||||
if (index === undefined) index = torrentSummary.defaultPlayFileIndex
|
|
||||||
if (index === undefined) index = TorrentPlayer.pickFileToPlay(torrentSummary.files)
|
if (index === undefined) index = torrentSummary.mostRecentFileIndex
|
||||||
|
if (index === undefined) index = torrentSummary.files.findIndex(TorrentPlayer.isPlayable)
|
||||||
if (index === undefined) return cb(new errors.UnplayableError())
|
if (index === undefined) return cb(new errors.UnplayableError())
|
||||||
|
|
||||||
|
state.playing.infoHash = torrentSummary.infoHash
|
||||||
|
|
||||||
// update UI to show pending playback
|
// update UI to show pending playback
|
||||||
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
if (torrentSummary.progress !== 1) sound.play('PLAY')
|
||||||
// TODO: remove torrentSummary.playStatus
|
// TODO: remove torrentSummary.playStatus
|
||||||
@@ -191,34 +230,68 @@ module.exports = class PlaybackController {
|
|||||||
this.update()
|
this.update()
|
||||||
}, 10000) /* give it a few seconds */
|
}, 10000) /* give it a few seconds */
|
||||||
|
|
||||||
|
this.startServer(torrentSummary, () => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
// if we timed out (user clicked play a long time ago), don't autoplay
|
||||||
|
var timedOut = torrentSummary.playStatus === 'timeout'
|
||||||
|
delete torrentSummary.playStatus
|
||||||
|
if (timedOut) {
|
||||||
|
ipcRenderer.send('wt-stop-server')
|
||||||
|
return this.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcRenderer.send('onPlayerOpen')
|
||||||
|
this.updatePlayer(infoHash, index, true, cb)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts WebTorrent server for media streaming
|
||||||
|
startServer (torrentSummary, cb) {
|
||||||
if (torrentSummary.status === 'paused') {
|
if (torrentSummary.status === 'paused') {
|
||||||
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
|
dispatch('startTorrentingSummary', torrentSummary.torrentKey)
|
||||||
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
ipcRenderer.once('wt-ready-' + torrentSummary.infoHash,
|
||||||
() => this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb))
|
() => onTorrentReady())
|
||||||
} else {
|
} else {
|
||||||
this.openPlayerFromActiveTorrent(torrentSummary, index, timeout, cb)
|
onTorrentReady()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTorrentReady () {
|
||||||
|
ipcRenderer.send('wt-start-server', torrentSummary.infoHash)
|
||||||
|
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, () => cb())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openPlayerFromActiveTorrent (torrentSummary, index, timeout, cb) {
|
// Called each time the current file changes
|
||||||
|
updatePlayer (infoHash, index, resume, cb) {
|
||||||
|
var state = this.state
|
||||||
|
|
||||||
|
var torrentSummary = TorrentSummary.getByKey(state, infoHash)
|
||||||
var fileSummary = torrentSummary.files[index]
|
var fileSummary = torrentSummary.files[index]
|
||||||
|
|
||||||
|
if (!TorrentPlayer.isPlayable(fileSummary)) {
|
||||||
|
torrentSummary.mostRecentFileIndex = undefined
|
||||||
|
return cb(new Error('Can\'t play that file'))
|
||||||
|
}
|
||||||
|
|
||||||
|
torrentSummary.mostRecentFileIndex = index
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
var state = this.state
|
|
||||||
state.playing.infoHash = torrentSummary.infoHash
|
|
||||||
state.playing.fileIndex = index
|
state.playing.fileIndex = index
|
||||||
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
|
state.playing.type = TorrentPlayer.isVideo(fileSummary) ? 'video'
|
||||||
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
: TorrentPlayer.isAudio(fileSummary) ? 'audio'
|
||||||
: 'other'
|
: 'other'
|
||||||
|
|
||||||
// pick up where we left off
|
// pick up where we left off
|
||||||
if (fileSummary.currentTime) {
|
var jumpToTime = 0
|
||||||
|
if (resume && fileSummary.currentTime) {
|
||||||
var fraction = fileSummary.currentTime / fileSummary.duration
|
var fraction = fileSummary.currentTime / fileSummary.duration
|
||||||
var secondsLeft = fileSummary.duration - fileSummary.currentTime
|
var secondsLeft = fileSummary.duration - fileSummary.currentTime
|
||||||
if (fraction < 0.9 && secondsLeft > 10) {
|
if (fraction < 0.9 && secondsLeft > 10) {
|
||||||
state.playing.jumpToTime = fileSummary.currentTime
|
jumpToTime = fileSummary.currentTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
state.playing.jumpToTime = jumpToTime
|
||||||
|
|
||||||
// if it's audio, parse out the metadata (artist, title, etc)
|
// if it's audio, parse out the metadata (artist, title, etc)
|
||||||
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
if (state.playing.type === 'audio' && !fileSummary.audioInfo) {
|
||||||
@@ -233,34 +306,21 @@ module.exports = class PlaybackController {
|
|||||||
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
|
dispatch('addSubtitles', [fileSummary.selectedSubtitle], true)
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcRenderer.send('wt-start-server', torrentSummary.infoHash, index)
|
state.window.title = fileSummary.name
|
||||||
ipcRenderer.once('wt-server-' + torrentSummary.infoHash, (e, info) => {
|
|
||||||
clearTimeout(timeout)
|
|
||||||
|
|
||||||
// if we timed out (user clicked play a long time ago), don't autoplay
|
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
|
||||||
var timedOut = torrentSummary.playStatus === 'timeout'
|
if (this.state.saved.prefs.openExternalPlayer) {
|
||||||
delete torrentSummary.playStatus
|
dispatch('openExternalPlayer')
|
||||||
if (timedOut) {
|
|
||||||
ipcRenderer.send('wt-stop-server')
|
|
||||||
return this.update()
|
|
||||||
}
|
|
||||||
|
|
||||||
state.window.title = torrentSummary.files[state.playing.fileIndex].name
|
|
||||||
|
|
||||||
// play in VLC if set as default player (Preferences / Playback / Play in VLC)
|
|
||||||
if (this.state.saved.prefs.openExternalPlayer) {
|
|
||||||
dispatch('openExternalPlayer')
|
|
||||||
this.update()
|
|
||||||
cb()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, play the video
|
|
||||||
this.update()
|
this.update()
|
||||||
|
|
||||||
ipcRenderer.send('onPlayerOpen')
|
|
||||||
cb()
|
cb()
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, play the video
|
||||||
|
this.update()
|
||||||
|
|
||||||
|
ipcRenderer.send('onPlayerUpdate', Playlist.hasNext(state), Playlist.hasPrevious(state))
|
||||||
|
cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
closePlayer () {
|
closePlayer () {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ const path = require('path')
|
|||||||
const ipcRenderer = require('electron').ipcRenderer
|
const ipcRenderer = require('electron').ipcRenderer
|
||||||
|
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
const TorrentPlayer = require('../lib/torrent-player')
|
|
||||||
const sound = require('../lib/sound')
|
const sound = require('../lib/sound')
|
||||||
const {dispatch} = require('../lib/dispatcher')
|
const {dispatch} = require('../lib/dispatcher')
|
||||||
|
|
||||||
@@ -73,7 +72,6 @@ module.exports = class TorrentController {
|
|||||||
if (!torrentSummary.selections) {
|
if (!torrentSummary.selections) {
|
||||||
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
torrentSummary.selections = torrentSummary.files.map((x) => true)
|
||||||
}
|
}
|
||||||
torrentSummary.defaultPlayFileIndex = TorrentPlayer.pickFileToPlay(torrentInfo.files)
|
|
||||||
dispatch('update')
|
dispatch('update')
|
||||||
|
|
||||||
// Save the .torrent file, if it hasn't been saved already
|
// Save the .torrent file, if it hasn't been saved already
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function chromecastPlayer () {
|
|||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||||
ret.device.play(state.server.networkURL, {
|
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
title: config.APP_NAME + ' - ' + torrentSummary.name
|
title: config.APP_NAME + ' - ' + torrentSummary.name
|
||||||
}, function (err) {
|
}, function (err) {
|
||||||
@@ -183,7 +183,7 @@ function airplayPlayer () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
ret.device.play(state.server.networkURL, function (err, res) {
|
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, function (err, res) {
|
||||||
if (err) {
|
if (err) {
|
||||||
state.playing.location = 'local'
|
state.playing.location = 'local'
|
||||||
state.errors.push({
|
state.errors.push({
|
||||||
@@ -275,7 +275,7 @@ function dlnaPlayer (player) {
|
|||||||
|
|
||||||
function open () {
|
function open () {
|
||||||
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
var torrentSummary = state.saved.torrents.find((x) => x.infoHash === state.playing.infoHash)
|
||||||
ret.device.play(state.server.networkURL, {
|
ret.device.play(state.server.networkURL + '/' + state.playing.fileIndex, {
|
||||||
type: 'video/mp4',
|
type: 'video/mp4',
|
||||||
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
title: config.APP_NAME + ' - ' + torrentSummary.name,
|
||||||
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
|
seek: state.playing.currentTime > 10 ? state.playing.currentTime : 0
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
UnplayableError
|
UnplayableTorrentError,
|
||||||
|
UnplayableFileError
|
||||||
}
|
}
|
||||||
|
|
||||||
function UnplayableError () {
|
function UnplayableTorrentError () {
|
||||||
this.message = 'Can\'t play any files in torrent'
|
this.message = 'Can\'t play any files in torrent'
|
||||||
}
|
}
|
||||||
UnplayableError.prototype = Error
|
|
||||||
|
function UnplayableFileError () {
|
||||||
|
this.message = 'Can\'t play that file'
|
||||||
|
}
|
||||||
|
|
||||||
|
UnplayableTorrentError.prototype = Error
|
||||||
|
UnplayableFileError.prototype = Error
|
||||||
|
|||||||
85
src/renderer/lib/playlist.js
Normal file
85
src/renderer/lib/playlist.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
module.exports = {
|
||||||
|
hasNext,
|
||||||
|
getNextIndex,
|
||||||
|
hasPrevious,
|
||||||
|
getPreviousIndex,
|
||||||
|
getCurrentLocalURL
|
||||||
|
}
|
||||||
|
|
||||||
|
const TorrentSummary = require('./torrent-summary')
|
||||||
|
const TorrentPlayer = require('./torrent-player')
|
||||||
|
|
||||||
|
var cache = {
|
||||||
|
infoHash: null,
|
||||||
|
previousIndex: null,
|
||||||
|
currentIndex: null,
|
||||||
|
nextIndex: null
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasNext (state) {
|
||||||
|
updateCache(state)
|
||||||
|
return cache.nextIndex !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextIndex (state) {
|
||||||
|
updateCache(state)
|
||||||
|
return cache.nextIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPrevious (state) {
|
||||||
|
updateCache(state)
|
||||||
|
return cache.previousIndex !== null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreviousIndex (state) {
|
||||||
|
updateCache(state)
|
||||||
|
return cache.previousIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentLocalURL (state) {
|
||||||
|
return state.server.localURL + '/' + state.playing.fileIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCache (state) {
|
||||||
|
var infoHash = state.playing.infoHash
|
||||||
|
var fileIndex = state.playing.fileIndex
|
||||||
|
|
||||||
|
if (infoHash === cache.infoHash) {
|
||||||
|
switch (fileIndex) {
|
||||||
|
case cache.currentIndex:
|
||||||
|
return
|
||||||
|
case cache.nextIndex:
|
||||||
|
cache.previousIndex = cache.currentIndex
|
||||||
|
cache.currentIndex = fileIndex
|
||||||
|
cache.nextIndex = findNextIndex(state)
|
||||||
|
return
|
||||||
|
case cache.previousIndex:
|
||||||
|
cache.previousIndex = findPreviousIndex(state)
|
||||||
|
cache.nextIndex = cache.currentIndex
|
||||||
|
cache.currentIndex = fileIndex
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cache.infoHash = infoHash
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.previousIndex = findPreviousIndex(state)
|
||||||
|
cache.currentIndex = fileIndex
|
||||||
|
cache.nextIndex = findNextIndex(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
function findPreviousIndex (state) {
|
||||||
|
var files = TorrentSummary.getByKey(state, state.playing.infoHash).files
|
||||||
|
for (var i = state.playing.fileIndex - 1; i >= 0; i--) {
|
||||||
|
if (TorrentPlayer.isPlayable(files[i])) return i
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNextIndex (state) {
|
||||||
|
var files = TorrentSummary.getByKey(state, state.playing.infoHash).files
|
||||||
|
for (var i = state.playing.fileIndex + 1; i < files.length; i++) {
|
||||||
|
if (TorrentPlayer.isPlayable(files[i])) return i
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -3,8 +3,7 @@ module.exports = {
|
|||||||
isVideo,
|
isVideo,
|
||||||
isAudio,
|
isAudio,
|
||||||
isTorrent,
|
isTorrent,
|
||||||
isPlayableTorrentSummary,
|
isPlayableTorrentSummary
|
||||||
pickFileToPlay
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = require('path')
|
var path = require('path')
|
||||||
@@ -60,25 +59,3 @@ function getFileExtension (file) {
|
|||||||
function isPlayableTorrentSummary (torrentSummary) {
|
function isPlayableTorrentSummary (torrentSummary) {
|
||||||
return torrentSummary.files && torrentSummary.files.some(isPlayable)
|
return torrentSummary.files && torrentSummary.files.some(isPlayable)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Picks the default file to play from a list of torrent or torrentSummary files
|
|
||||||
// Returns an index or undefined, if no files are playable
|
|
||||||
function pickFileToPlay (files) {
|
|
||||||
// first, try to find the biggest video file
|
|
||||||
var videoFiles = files.filter(isVideo)
|
|
||||||
if (videoFiles.length > 0) {
|
|
||||||
var largestVideoFile = videoFiles.reduce(function (a, b) {
|
|
||||||
return a.length > b.length ? a : b
|
|
||||||
})
|
|
||||||
return files.indexOf(largestVideoFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there are no videos, play the first audio file
|
|
||||||
var audioFiles = files.filter(isAudio)
|
|
||||||
if (audioFiles.length > 0) {
|
|
||||||
return files.indexOf(audioFiles[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// no video or audio means nothing is playable
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ const dispatchHandlers = {
|
|||||||
// Playback
|
// Playback
|
||||||
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
'playFile': (infoHash, index) => controllers.playback.playFile(infoHash, index),
|
||||||
'playPause': () => controllers.playback.playPause(),
|
'playPause': () => controllers.playback.playPause(),
|
||||||
|
'nextTrack': () => controllers.playback.nextTrack(),
|
||||||
|
'previousTrack': () => controllers.playback.previousTrack(),
|
||||||
'skip': (time) => controllers.playback.skip(time),
|
'skip': (time) => controllers.playback.skip(time),
|
||||||
'skipTo': (time) => controllers.playback.skipTo(time),
|
'skipTo': (time) => controllers.playback.skipTo(time),
|
||||||
'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir),
|
'changePlaybackRate': (dir) => controllers.playback.changePlaybackRate(dir),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const zeroFill = require('zero-fill')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
const TorrentSummary = require('../lib/torrent-summary')
|
const TorrentSummary = require('../lib/torrent-summary')
|
||||||
|
const Playlist = require('../lib/playlist')
|
||||||
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
const {dispatch, dispatcher} = require('../lib/dispatcher')
|
||||||
|
|
||||||
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
||||||
@@ -109,7 +110,7 @@ function renderMedia (state) {
|
|||||||
var MediaTagName = state.playing.type
|
var MediaTagName = state.playing.type
|
||||||
var mediaTag = (
|
var mediaTag = (
|
||||||
<MediaTagName
|
<MediaTagName
|
||||||
src={state.server.localURL}
|
src={Playlist.getCurrentLocalURL(state)}
|
||||||
onDoubleClick={dispatcher('toggleFullScreen')}
|
onDoubleClick={dispatcher('toggleFullScreen')}
|
||||||
onLoadedMetadata={onLoadedMetadata}
|
onLoadedMetadata={onLoadedMetadata}
|
||||||
onEnded={onEnded}
|
onEnded={onEnded}
|
||||||
@@ -144,9 +145,13 @@ function renderMedia (state) {
|
|||||||
dispatch('setDimensions', dimensions)
|
dispatch('setDimensions', dimensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the video completes, pause the video instead of looping
|
|
||||||
function onEnded (e) {
|
function onEnded (e) {
|
||||||
state.playing.isPaused = true
|
if (Playlist.hasNext(state)) {
|
||||||
|
dispatch('nextTrack')
|
||||||
|
} else {
|
||||||
|
// When the last video completes, pause the video instead of looping
|
||||||
|
state.playing.isPaused = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCanPlay (e) {
|
function onCanPlay (e) {
|
||||||
@@ -378,6 +383,8 @@ function renderPlayerControls (state) {
|
|||||||
: state.playing.subtitles.selectedIndex >= 0
|
: state.playing.subtitles.selectedIndex >= 0
|
||||||
? 'active'
|
? 'active'
|
||||||
: ''
|
: ''
|
||||||
|
var prevClass = Playlist.hasPrevious(state) ? '' : 'disabled'
|
||||||
|
var nextClass = Playlist.hasNext(state) ? '' : 'disabled'
|
||||||
|
|
||||||
var elements = [
|
var elements = [
|
||||||
<div key='playback-bar' className='playback-bar'>
|
<div key='playback-bar' className='playback-bar'>
|
||||||
@@ -397,6 +404,13 @@ function renderPlayerControls (state) {
|
|||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
|
|
||||||
|
<i
|
||||||
|
key='skip-previous'
|
||||||
|
className={'icon skip-previous float-left ' + prevClass}
|
||||||
|
onClick={dispatcher('previousTrack')}>
|
||||||
|
skip_previous
|
||||||
|
</i>,
|
||||||
|
|
||||||
<i
|
<i
|
||||||
key='play'
|
key='play'
|
||||||
className='icon play-pause float-left'
|
className='icon play-pause float-left'
|
||||||
@@ -404,6 +418,13 @@ function renderPlayerControls (state) {
|
|||||||
{state.playing.isPaused ? 'play_arrow' : 'pause'}
|
{state.playing.isPaused ? 'play_arrow' : 'pause'}
|
||||||
</i>,
|
</i>,
|
||||||
|
|
||||||
|
<i
|
||||||
|
key='skip-next'
|
||||||
|
className={'icon skip-next float-left ' + nextClass}
|
||||||
|
onClick={dispatcher('nextTrack')}>
|
||||||
|
skip_next
|
||||||
|
</i>,
|
||||||
|
|
||||||
<i
|
<i
|
||||||
key='fullscreen'
|
key='fullscreen'
|
||||||
className='icon fullscreen float-right'
|
className='icon fullscreen float-right'
|
||||||
|
|||||||
@@ -207,10 +207,10 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
// Do we have a saved position? Show it using a radial progress bar on top
|
// Do we have a saved position? Show it using a radial progress bar on top
|
||||||
// of the play button, unless already showing a spinner there:
|
// of the play button, unless already showing a spinner there:
|
||||||
var willShowSpinner = torrentSummary.playStatus === 'requested'
|
var willShowSpinner = torrentSummary.playStatus === 'requested'
|
||||||
var defaultFile = torrentSummary.files &&
|
var mostRecentFile = torrentSummary.files &&
|
||||||
torrentSummary.files[torrentSummary.defaultPlayFileIndex]
|
torrentSummary.files[torrentSummary.mostRecentFileIndex]
|
||||||
if (defaultFile && defaultFile.currentTime && !willShowSpinner) {
|
if (mostRecentFile && mostRecentFile.currentTime && !willShowSpinner) {
|
||||||
var fraction = defaultFile.currentTime / defaultFile.duration
|
var fraction = mostRecentFile.currentTime / mostRecentFile.duration
|
||||||
positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large')
|
positionElem = this.renderRadialProgressBar(fraction, 'radial-progress-large')
|
||||||
playClass = 'resume-position'
|
playClass = 'resume-position'
|
||||||
}
|
}
|
||||||
@@ -273,11 +273,6 @@ module.exports = class TorrentList extends React.Component {
|
|||||||
var fileRows = torrentSummary.files
|
var fileRows = torrentSummary.files
|
||||||
.filter((file) => !file.path.includes('/.____padding_file/'))
|
.filter((file) => !file.path.includes('/.____padding_file/'))
|
||||||
.map((file, index) => ({ file, index }))
|
.map((file, index) => ({ file, index }))
|
||||||
.sort(function (a, b) {
|
|
||||||
if (a.file.name < b.file.name) return -1
|
|
||||||
if (b.file.name < a.file.name) return 1
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
|
.map((object) => this.renderFileRow(torrentSummary, object.file, object.index))
|
||||||
|
|
||||||
filesElement = (
|
filesElement = (
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ function init () {
|
|||||||
generateTorrentPoster(torrentKey))
|
generateTorrentPoster(torrentKey))
|
||||||
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
|
ipc.on('wt-get-audio-metadata', (e, infoHash, index) =>
|
||||||
getAudioMetadata(infoHash, index))
|
getAudioMetadata(infoHash, index))
|
||||||
ipc.on('wt-start-server', (e, infoHash, index) =>
|
ipc.on('wt-start-server', (e, infoHash) =>
|
||||||
startServer(infoHash, index))
|
startServer(infoHash))
|
||||||
ipc.on('wt-stop-server', (e) =>
|
ipc.on('wt-stop-server', (e) =>
|
||||||
stopServer())
|
stopServer())
|
||||||
ipc.on('wt-select-files', (e, infoHash, selections) =>
|
ipc.on('wt-select-files', (e, infoHash, selections) =>
|
||||||
@@ -301,20 +301,20 @@ function getTorrentProgress () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startServer (infoHash, index) {
|
function startServer (infoHash) {
|
||||||
var torrent = client.get(infoHash)
|
var torrent = client.get(infoHash)
|
||||||
if (torrent.ready) startServerFromReadyTorrent(torrent, index)
|
if (torrent.ready) startServerFromReadyTorrent(torrent)
|
||||||
else torrent.once('ready', () => startServerFromReadyTorrent(torrent, index))
|
else torrent.once('ready', () => startServerFromReadyTorrent(torrent))
|
||||||
}
|
}
|
||||||
|
|
||||||
function startServerFromReadyTorrent (torrent, index, cb) {
|
function startServerFromReadyTorrent (torrent, cb) {
|
||||||
if (server) return
|
if (server) return
|
||||||
|
|
||||||
// start the streaming torrent-to-http server
|
// start the streaming torrent-to-http server
|
||||||
server = torrent.createServer()
|
server = torrent.createServer()
|
||||||
server.listen(0, function () {
|
server.listen(0, function () {
|
||||||
var port = server.address().port
|
var port = server.address().port
|
||||||
var urlSuffix = ':' + port + '/' + index
|
var urlSuffix = ':' + port
|
||||||
var info = {
|
var info = {
|
||||||
torrentKey: torrent.key,
|
torrentKey: torrent.key,
|
||||||
localURL: 'http://localhost' + urlSuffix,
|
localURL: 'http://localhost' + urlSuffix,
|
||||||
@@ -322,7 +322,7 @@ function startServerFromReadyTorrent (torrent, index, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipc.send('wt-server-running', info)
|
ipc.send('wt-server-running', info)
|
||||||
ipc.send('wt-server-' + torrent.infoHash, info) // TODO: hack
|
ipc.send('wt-server-' + torrent.infoHash, info)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
static/NextTrackThumbnailBarButton.png
Normal file
BIN
static/NextTrackThumbnailBarButton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 225 B |
BIN
static/PreviousTrackThumbnailBarButton.png
Normal file
BIN
static/PreviousTrackThumbnailBarButton.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 233 B |
@@ -634,7 +634,25 @@ body.drag .app::after {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player .controls .play-pause {
|
.player .controls .icon.disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .icon.skip-previous {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .icon.play-pause {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player .controls .icon.skip-next {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user