Chromecast video controls
This commit is contained in:
@@ -643,9 +643,10 @@ body.drag .torrent-placeholder span {
|
|||||||
|
|
||||||
.player-controls .play-pause {
|
.player-controls .play-pause {
|
||||||
display: block;
|
display: block;
|
||||||
width: 20px;
|
width: 30px;
|
||||||
height: 20px;
|
height: 30px;
|
||||||
margin: 5px auto;
|
padding: 5px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-controls .chromecast,
|
.player-controls .chromecast,
|
||||||
@@ -674,6 +675,11 @@ body.drag .torrent-placeholder span {
|
|||||||
margin-top: 8px !important;
|
margin-top: 8px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-controls .chromecast.active,
|
||||||
|
.player-controls .airplay.active {
|
||||||
|
color: #9af;
|
||||||
|
}
|
||||||
|
|
||||||
.player .playback-bar:hover .loading-bar {
|
.player .playback-bar:hover .loading-bar {
|
||||||
height: 5px;
|
height: 5px;
|
||||||
}
|
}
|
||||||
@@ -686,6 +692,33 @@ body.drag .torrent-placeholder span {
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* CHROMECAST / AIRPLAY CONTROLS
|
||||||
|
*/
|
||||||
|
.cast-screen {
|
||||||
|
width: 400px;
|
||||||
|
margin: auto;
|
||||||
|
color: #eee;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cast-screen h1 {
|
||||||
|
font-size: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cast-screen .cast-status {
|
||||||
|
margin: 40px 0;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cast-screen .stop-casting {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cast-screen .stop-casting:hover {
|
||||||
|
color: #9af;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MEDIA QUERIES
|
* MEDIA QUERIES
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
console.time('init')
|
console.time('init')
|
||||||
|
|
||||||
var airplay = require('airplay-js')
|
|
||||||
var cfg = require('application-config')('WebTorrent')
|
var cfg = require('application-config')('WebTorrent')
|
||||||
var cfgDirectory = require('application-config-path')('WebTorrent')
|
var cfgDirectory = require('application-config-path')('WebTorrent')
|
||||||
var chromecasts = require('chromecasts')()
|
|
||||||
var createTorrent = require('create-torrent')
|
var createTorrent = require('create-torrent')
|
||||||
var dragDrop = require('drag-drop')
|
var dragDrop = require('drag-drop')
|
||||||
var electron = require('electron')
|
var electron = require('electron')
|
||||||
@@ -22,6 +20,7 @@ var App = require('./views/app')
|
|||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var torrentPoster = require('./lib/torrent-poster')
|
var torrentPoster = require('./lib/torrent-poster')
|
||||||
var TorrentPlayer = require('./lib/torrent-player')
|
var TorrentPlayer = require('./lib/torrent-player')
|
||||||
|
var Cast = require('./lib/cast')
|
||||||
|
|
||||||
// Electron apps have two processes: a main process (node) runs first and starts
|
// Electron apps have two processes: a main process (node) runs first and starts
|
||||||
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
// a renderer process (essentially a Chrome window). We're in the renderer process,
|
||||||
@@ -86,7 +85,7 @@ function init () {
|
|||||||
|
|
||||||
// OS integrations:
|
// OS integrations:
|
||||||
// ...Chromecast and Airplay
|
// ...Chromecast and Airplay
|
||||||
detectDevices()
|
Cast.init(update)
|
||||||
|
|
||||||
// ...drag and drop a torrent or video file to play or seed
|
// ...drag and drop a torrent or video file to play or seed
|
||||||
dragDrop('body', onFiles)
|
dragDrop('body', onFiles)
|
||||||
@@ -188,10 +187,13 @@ function dispatch (action, ...args) {
|
|||||||
toggleSelectTorrent(args[0] /* infoHash */)
|
toggleSelectTorrent(args[0] /* infoHash */)
|
||||||
}
|
}
|
||||||
if (action === 'openChromecast') {
|
if (action === 'openChromecast') {
|
||||||
openChromecast()
|
Cast.openChromecast()
|
||||||
}
|
}
|
||||||
if (action === 'openAirplay') {
|
if (action === 'openAirplay') {
|
||||||
openAirplay()
|
Cast.openAirplay()
|
||||||
|
}
|
||||||
|
if (action === 'stopCasting') {
|
||||||
|
Cast.stopCasting()
|
||||||
}
|
}
|
||||||
if (action === 'setDimensions') {
|
if (action === 'setDimensions') {
|
||||||
setDimensions(args[0] /* dimensions */)
|
setDimensions(args[0] /* dimensions */)
|
||||||
@@ -206,12 +208,11 @@ function dispatch (action, ...args) {
|
|||||||
// TODO
|
// TODO
|
||||||
// window.history.forward()
|
// window.history.forward()
|
||||||
}
|
}
|
||||||
if (action === 'pause') {
|
if (action === 'playPause') {
|
||||||
if (state.url !== 'player' || state.video.isPaused) {
|
playPause()
|
||||||
ipcRenderer.send('paused-video')
|
}
|
||||||
}
|
if (action === 'playbackJump') {
|
||||||
state.video.isPaused = true
|
jumpToTime(args[0] /* seconds */)
|
||||||
update()
|
|
||||||
}
|
}
|
||||||
if (action === 'videoPlaying') {
|
if (action === 'videoPlaying') {
|
||||||
ipcRenderer.send('blockPowerSave')
|
ipcRenderer.send('blockPowerSave')
|
||||||
@@ -220,14 +221,6 @@ function dispatch (action, ...args) {
|
|||||||
ipcRenderer.send('paused-video')
|
ipcRenderer.send('paused-video')
|
||||||
ipcRenderer.send('unblockPowerSave')
|
ipcRenderer.send('unblockPowerSave')
|
||||||
}
|
}
|
||||||
if (action === 'playPause') {
|
|
||||||
state.video.isPaused = !state.video.isPaused
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
if (action === 'playbackJump') {
|
|
||||||
state.video.jumpToTime = args[0] /* seconds */
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
if (action === 'toggleFullScreen') {
|
if (action === 'toggleFullScreen') {
|
||||||
ipcRenderer.send('toggleFullScreen', args[0])
|
ipcRenderer.send('toggleFullScreen', args[0])
|
||||||
update()
|
update()
|
||||||
@@ -242,6 +235,23 @@ function dispatch (action, ...args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playPause () {
|
||||||
|
if (Cast.isCasting()) {
|
||||||
|
Cast.playPause()
|
||||||
|
}
|
||||||
|
state.video.isPaused = !state.video.isPaused
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
function jumpToTime (time) {
|
||||||
|
if (Cast.isCasting()) {
|
||||||
|
Cast.seek(time)
|
||||||
|
} else {
|
||||||
|
state.video.jumpToTime = time
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setupIpc () {
|
function setupIpc () {
|
||||||
ipcRenderer.on('dispatch', function (e, action, ...args) {
|
ipcRenderer.on('dispatch', function (e, action, ...args) {
|
||||||
dispatch(action, ...args)
|
dispatch(action, ...args)
|
||||||
@@ -265,16 +275,6 @@ function setupIpc () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectDevices () {
|
|
||||||
chromecasts.on('update', function (player) {
|
|
||||||
state.devices.chromecast = player
|
|
||||||
})
|
|
||||||
|
|
||||||
airplay.createBrowser().on('deviceOn', function (player) {
|
|
||||||
state.devices.airplay = player
|
|
||||||
}).start()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load state.saved from the JSON state file
|
// Load state.saved from the JSON state file
|
||||||
function loadState (callback) {
|
function loadState (callback) {
|
||||||
cfg.read(function (err, data) {
|
cfg.read(function (err, data) {
|
||||||
@@ -599,25 +599,6 @@ function toggleSelectTorrent (infoHash) {
|
|||||||
update()
|
update()
|
||||||
}
|
}
|
||||||
|
|
||||||
function openChromecast () {
|
|
||||||
var torrentSummary = getTorrentSummary(state.playing.infoHash)
|
|
||||||
state.devices.chromecast.play(state.server.networkURL, {
|
|
||||||
title: config.APP_NAME + ' — ' + torrentSummary.name
|
|
||||||
})
|
|
||||||
state.devices.chromecast.on('error', function (err) {
|
|
||||||
err.message = 'Chromecast: ' + err.message
|
|
||||||
onError(err)
|
|
||||||
})
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAirplay () {
|
|
||||||
state.devices.airplay.play(state.server.networkURL, 0, function () {
|
|
||||||
// TODO: handle airplay errors
|
|
||||||
})
|
|
||||||
update()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set window dimensions to match video dimensions or fill the screen
|
// Set window dimensions to match video dimensions or fill the screen
|
||||||
function setDimensions (dimensions) {
|
function setDimensions (dimensions) {
|
||||||
state.window.bounds = {
|
state.window.bounds = {
|
||||||
|
|||||||
151
renderer/lib/cast.js
Normal file
151
renderer/lib/cast.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
var chromecasts = require('chromecasts')()
|
||||||
|
var airplay = require('airplay-js')
|
||||||
|
|
||||||
|
var config = require('../../config')
|
||||||
|
var state = require('../state')
|
||||||
|
|
||||||
|
// The Cast module talks to Airplay and Chromecast
|
||||||
|
// * Modifies state when things change
|
||||||
|
// * Starts and stops casting, provides remote video controls
|
||||||
|
module.exports = {
|
||||||
|
init,
|
||||||
|
openChromecast,
|
||||||
|
openAirplay,
|
||||||
|
stopCasting,
|
||||||
|
playPause,
|
||||||
|
seek,
|
||||||
|
isCasting
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback to notify module users when state has changed
|
||||||
|
var update
|
||||||
|
|
||||||
|
function init (callback) {
|
||||||
|
update = callback
|
||||||
|
|
||||||
|
// Start polling Chromecast or Airplay, whenever we're connected
|
||||||
|
setInterval(() => pollCastStatus(state), 1000)
|
||||||
|
|
||||||
|
// Listen for devices: Chromecast and Airplay
|
||||||
|
chromecasts.on('update', function (player) {
|
||||||
|
state.devices.chromecast = player
|
||||||
|
addChromecastEvents()
|
||||||
|
})
|
||||||
|
|
||||||
|
airplay.createBrowser().on('deviceOn', function (player) {
|
||||||
|
state.devices.airplay = player
|
||||||
|
addAirplayEvents()
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
function addChromecastEvents () {
|
||||||
|
state.devices.chromecast.on('error', function (err) {
|
||||||
|
err.message = 'Chromecast: ' + err.message
|
||||||
|
onError(err)
|
||||||
|
})
|
||||||
|
state.devices.chromecast.on('disconnect', function () {
|
||||||
|
state.playing.location = 'local'
|
||||||
|
update()
|
||||||
|
})
|
||||||
|
state.devices.chromecast.on('status', handleStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAirplayEvents () {}
|
||||||
|
|
||||||
|
// Update our state from the remote TV
|
||||||
|
function pollCastStatus(state) {
|
||||||
|
var device
|
||||||
|
if (state.playing.location === 'chromecast') device = state.devices.chromecast
|
||||||
|
else if (state.playing.location === 'airplay') device = state.devices.airplay
|
||||||
|
else return
|
||||||
|
|
||||||
|
device.status(function (err, status) {
|
||||||
|
if (err) {
|
||||||
|
return console.log('Error retrieving %s status: %o', state.playing.location, err)
|
||||||
|
}
|
||||||
|
console.log('GOT CAST STATUS: %o', status)
|
||||||
|
handleStatus (status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStatus (status) {
|
||||||
|
state.video.isCastPaused = status.playerState === 'PAUSED'
|
||||||
|
state.video.currentTime = status.currentTime
|
||||||
|
}
|
||||||
|
|
||||||
|
function openChromecast () {
|
||||||
|
if (state.playing.location !== 'local') {
|
||||||
|
throw new Error('You can\'t connect to Chromecast when already connected to another device')
|
||||||
|
}
|
||||||
|
|
||||||
|
state.playing.location = 'chromecast-pending'
|
||||||
|
var torrentSummary = getTorrentSummary(state.playing.infoHash)
|
||||||
|
state.devices.chromecast.play(state.server.networkURL, {
|
||||||
|
type: 'video/mp4',
|
||||||
|
title: config.APP_NAME + ' — ' + torrentSummary.name
|
||||||
|
}, function (err) {
|
||||||
|
state.playing.location = err ? 'local' : 'chromecast'
|
||||||
|
update()
|
||||||
|
})
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
function openAirplay () {
|
||||||
|
if (state.playing.location !== 'local') {
|
||||||
|
throw new Error('You can\'t connect to Airplay when already connected to another device')
|
||||||
|
}
|
||||||
|
|
||||||
|
state.playing.location = 'airplay-pending'
|
||||||
|
state.devices.airplay.play(state.server.networkURL, 0, function () {
|
||||||
|
console.log('Airplay', arguments) // TODO: handle airplay errors
|
||||||
|
state.playing.location = 'airplay'
|
||||||
|
update()
|
||||||
|
})
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stops Chromecast or Airplay, move video back to local screen
|
||||||
|
function stopCasting () {
|
||||||
|
if (state.playing.location === 'chromecast') {
|
||||||
|
state.devices.chromecast.stop(stoppedCasting)
|
||||||
|
} else if (state.playing.location === 'airplay') {
|
||||||
|
throw new Error('Unimplemented') // TODO stop airplay
|
||||||
|
} else if (state.playing.location.endsWith('-pending')) {
|
||||||
|
// Connecting to Chromecast took too long or errored out. Let the user cancel
|
||||||
|
stoppedCasting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stoppedCasting () {
|
||||||
|
state.playing.location = 'local'
|
||||||
|
state.video.jumpToTime = state.video.currentTime
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
function isCasting () {
|
||||||
|
return state.playing.location === 'chromecast' || state.playing.location === 'airplay'
|
||||||
|
}
|
||||||
|
|
||||||
|
function playPause () {
|
||||||
|
var device = getActiveDevice()
|
||||||
|
if (!state.video.isPaused) device.pause(castCallback)
|
||||||
|
else device.play(null, null, castCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function seek (time) {
|
||||||
|
var device = getActiveDevice()
|
||||||
|
device.seek(time, castCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveDevice () {
|
||||||
|
if (state.playing.location === 'chromecast') return state.devices.chromecast
|
||||||
|
else if (state.playing.location === 'airplay') return state.devices.airplay
|
||||||
|
else throw new Error('getActiveDevice() called, but we\'re not casting')
|
||||||
|
}
|
||||||
|
|
||||||
|
function castCallback (err) {
|
||||||
|
console.log('CAST CALLBACK: %o', arguments)
|
||||||
|
}
|
||||||
@@ -11,34 +11,33 @@ module.exports = {
|
|||||||
client: null, /* the WebTorrent client */
|
client: null, /* the WebTorrent client */
|
||||||
server: null, /* local WebTorrent-to-HTTP server */
|
server: null, /* local WebTorrent-to-HTTP server */
|
||||||
prev: {}, /* used for state diffing in updateElectron() */
|
prev: {}, /* used for state diffing in updateElectron() */
|
||||||
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
|
|
||||||
playing: {
|
|
||||||
infoHash: null, /* the info hash of the torrent we're playing */
|
|
||||||
fileIndex: null /* the zero-based index within the torrent */
|
|
||||||
},
|
|
||||||
// history: [], /* track how we got to the current view. enables Back button */
|
|
||||||
// historyIndex: 0,
|
|
||||||
url: 'home',
|
url: 'home',
|
||||||
devices: {
|
|
||||||
airplay: null, /* airplay client. finds and manages AppleTVs */
|
|
||||||
chromecast: null /* chromecast client. finds and manages Chromecasts */
|
|
||||||
},
|
|
||||||
dock: {
|
|
||||||
badge: 0,
|
|
||||||
progress: 0
|
|
||||||
},
|
|
||||||
window: {
|
window: {
|
||||||
bounds: null, /* x y width height */
|
bounds: null, /* x y width height */
|
||||||
isFocused: true,
|
isFocused: true,
|
||||||
isFullScreen: false,
|
isFullScreen: false,
|
||||||
title: config.APP_NAME /* current window title */
|
title: config.APP_NAME /* current window title */
|
||||||
},
|
},
|
||||||
video: {
|
selectedInfoHash: null, /* the torrent we've selected to view details. see state.torrents */
|
||||||
|
playing: { /* the torrent and file we're currently streaming */
|
||||||
|
infoHash: null, /* the info hash of the torrent we're playing */
|
||||||
|
fileIndex: null, /* the zero-based index within the torrent */
|
||||||
|
location: 'local' /* 'local', 'chromecast', 'airplay' */
|
||||||
|
},
|
||||||
|
devices: { /* playback devices like Chromecast and AppleTV */
|
||||||
|
airplay: null, /* airplay client. finds and manages AppleTVs */
|
||||||
|
chromecast: null /* chromecast client. finds and manages Chromecasts */
|
||||||
|
},
|
||||||
|
video: { /* state of the video player screen */
|
||||||
currentTime: 0, /* seconds */
|
currentTime: 0, /* seconds */
|
||||||
duration: 1, /* seconds */
|
duration: 1, /* seconds */
|
||||||
isPaused: false,
|
isPaused: false,
|
||||||
mouseStationarySince: 0 /* Unix time in ms */
|
mouseStationarySince: 0 /* Unix time in ms */
|
||||||
},
|
},
|
||||||
|
dock: {
|
||||||
|
badge: 0,
|
||||||
|
progress: 0
|
||||||
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Saved state is read from and written to a file every time the app runs.
|
* Saved state is read from and written to a file every time the app runs.
|
||||||
|
|||||||
@@ -16,10 +16,12 @@ function App (state, dispatch) {
|
|||||||
// Never hide the controls when:
|
// Never hide the controls when:
|
||||||
// * The mouse is over the controls or we're scrubbing (see CSS)
|
// * The mouse is over the controls or we're scrubbing (see CSS)
|
||||||
// * The video is paused
|
// * The video is paused
|
||||||
|
// * The video is playing remotely on Chromecast or Airplay
|
||||||
var hideControls = state.url === 'player' &&
|
var hideControls = state.url === 'player' &&
|
||||||
state.video.mouseStationarySince !== 0 &&
|
state.video.mouseStationarySince !== 0 &&
|
||||||
new Date().getTime() - state.video.mouseStationarySince > 2000 &&
|
new Date().getTime() - state.video.mouseStationarySince > 2000 &&
|
||||||
!state.video.isPaused
|
!state.video.isPaused &&
|
||||||
|
state.video.location === 'local'
|
||||||
|
|
||||||
var cls = [
|
var cls = [
|
||||||
'view-' + state.url, /* e.g. view-home, view-player */
|
'view-' + state.url, /* e.g. view-home, view-player */
|
||||||
|
|||||||
@@ -4,7 +4,22 @@ var h = require('virtual-dom/h')
|
|||||||
var hyperx = require('hyperx')
|
var hyperx = require('hyperx')
|
||||||
var hx = hyperx(h)
|
var hx = hyperx(h)
|
||||||
|
|
||||||
|
// Shows a streaming video player. Standard features + Chromecast + Airplay
|
||||||
function Player (state, dispatch) {
|
function Player (state, dispatch) {
|
||||||
|
// Show the video as large as will fit in the window, play immediately
|
||||||
|
// If the video is on Chromecast or Airplay, show a title screen instead
|
||||||
|
var showVideo = state.playing.location === 'local'
|
||||||
|
return hx`
|
||||||
|
<div
|
||||||
|
class='player'
|
||||||
|
onmousemove=${() => dispatch('videoMouseMoved')}>
|
||||||
|
${showVideo ? renderVideo(state, dispatch) : renderCastScreen(state, dispatch)}
|
||||||
|
${renderPlayerControls(state, dispatch)}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderVideo (state, dispatch) {
|
||||||
// Unfortunately, play/pause can't be done just by modifying HTML.
|
// Unfortunately, play/pause can't be done just by modifying HTML.
|
||||||
// Instead, grab the DOM node and play/pause it if necessary
|
// Instead, grab the DOM node and play/pause it if necessary
|
||||||
var videoElement = document.querySelector('video')
|
var videoElement = document.querySelector('video')
|
||||||
@@ -23,25 +38,19 @@ function Player (state, dispatch) {
|
|||||||
state.video.duration = videoElement.duration
|
state.video.duration = videoElement.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the video as large as will fit in the window, play immediately
|
|
||||||
return hx`
|
return hx`
|
||||||
<div
|
<div
|
||||||
class='player'
|
class='letterbox'
|
||||||
onmousemove=${() => dispatch('videoMouseMoved')}>
|
onmousemove=${() => dispatch('videoMouseMoved')}>
|
||||||
<div
|
<video
|
||||||
class='letterbox'
|
src='${state.server.localURL}'
|
||||||
onmousemove=${() => dispatch('videoMouseMoved')}>
|
ondblclick=${() => dispatch('toggleFullScreen')}
|
||||||
<video
|
onloadedmetadata=${onLoadedMetadata}
|
||||||
src='${state.server.localURL}'
|
onended=${onEnded}
|
||||||
ondblclick=${() => dispatch('toggleFullScreen')}
|
onplay=${() => dispatch('videoPlaying')}
|
||||||
onloadedmetadata=${onLoadedMetadata}
|
onpause=${() => dispatch('videoPaused')}
|
||||||
onended=${onEnded}
|
autoplay>
|
||||||
onplay=${() => dispatch('videoPlaying')}
|
</video>
|
||||||
onpause=${() => dispatch('videoPaused')}
|
|
||||||
autoplay>
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
${renderPlayerControls(state, dispatch)}
|
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -61,6 +70,39 @@ function Player (state, dispatch) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderCastScreen (state, dispatch) {
|
||||||
|
var isChromecast = state.playing.location.startsWith('chromecast')
|
||||||
|
var isAirplay = state.playing.location.startsWith('airplay')
|
||||||
|
var isStarting = state.playing.location.endsWith('-pending')
|
||||||
|
if (!isChromecast && !isAirplay) throw new Error('Unimplemented cast type')
|
||||||
|
|
||||||
|
// Finally, show a static title screen and the cast status
|
||||||
|
var header = isChromecast ? 'Chromecast' : 'AirPlay'
|
||||||
|
var content
|
||||||
|
if (isStarting) {
|
||||||
|
content = hx`
|
||||||
|
<div class='cast-status'>Connecting...</div>
|
||||||
|
`
|
||||||
|
} else {
|
||||||
|
content = hx`
|
||||||
|
<div class='cast-status'>
|
||||||
|
<div class='button stop-casting'
|
||||||
|
onclick=${() => dispatch('stopCasting')}>
|
||||||
|
Stop Casting
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
return hx`
|
||||||
|
<div class='letterbox'>
|
||||||
|
<div class='cast-screen'>
|
||||||
|
<h1>${header}</h1>
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
function renderPlayerControls (state, dispatch) {
|
function renderPlayerControls (state, dispatch) {
|
||||||
var positionPercent = 100 * state.video.currentTime / state.video.duration
|
var positionPercent = 100 * state.video.currentTime / state.video.duration
|
||||||
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' }
|
||||||
@@ -79,27 +121,50 @@ function renderPlayerControls (state, dispatch) {
|
|||||||
hx`
|
hx`
|
||||||
<i class='icon fullscreen'
|
<i class='icon fullscreen'
|
||||||
onclick=${() => dispatch('toggleFullScreen')}>
|
onclick=${() => dispatch('toggleFullScreen')}>
|
||||||
fullscreen
|
${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'}
|
||||||
</i>
|
</i>
|
||||||
`
|
`
|
||||||
]
|
]
|
||||||
|
|
||||||
// If we've detected a Chromecast or AppleTV, the user can play video there
|
// If we've detected a Chromecast or AppleTV, the user can play video there
|
||||||
if (state.devices.chromecast) {
|
var isOnChromecast = state.playing.location.startsWith('chromecast')
|
||||||
|
var isOnAirplay = state.playing.location.startsWith('airplay')
|
||||||
|
var chromecastClass, chromecastHandler, airplayClass, airplayHandler
|
||||||
|
if (isOnChromecast) {
|
||||||
|
chromecastClass = 'active'
|
||||||
|
airplayClass = 'disabled'
|
||||||
|
chromecastHandler = () => dispatch('stopCasting')
|
||||||
|
airplayHandler = undefined
|
||||||
|
} else if (isOnAirplay) {
|
||||||
|
chromecastClass = 'disabled'
|
||||||
|
airplayClass = 'active'
|
||||||
|
chromecastHandler = undefined
|
||||||
|
airplayHandler = () => dispatch('stopCasting')
|
||||||
|
} else {
|
||||||
|
chromecastClass = ''
|
||||||
|
airplayClass = ''
|
||||||
|
chromecastHandler = () => dispatch('openChromecast')
|
||||||
|
airplayHandler = () => dispatch('openAirplay')
|
||||||
|
}
|
||||||
|
if (state.devices.chromecast || isOnChromecast) {
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i.icon.chromecast
|
<i.icon.chromecast
|
||||||
onclick=${() => dispatch('openChromecast')}>
|
class=${chromecastClass}
|
||||||
|
onclick=${chromecastHandler}>
|
||||||
cast
|
cast
|
||||||
</i>
|
</i>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
if (state.devices.airplay) {
|
if (state.devices.airplay || isOnAirplay) {
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i.icon.airplay
|
<i.icon.airplay
|
||||||
onclick=${() => dispatch('openAirplay')}>
|
class=${airplayClass}
|
||||||
|
onclick=${airplayHandler}>
|
||||||
airplay
|
airplay
|
||||||
</i>
|
</i>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// On OSX, the back button is in the title bar of the window; see app.js
|
// On OSX, the back button is in the title bar of the window; see app.js
|
||||||
// On other platforms, we render one over the video on mouseover
|
// On other platforms, we render one over the video on mouseover
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
@@ -110,6 +175,8 @@ function renderPlayerControls (state, dispatch) {
|
|||||||
</i>
|
</i>
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finally, the big button in the center plays or pauses the video
|
||||||
elements.push(hx`
|
elements.push(hx`
|
||||||
<i class='icon play-pause' onclick=${() => dispatch('playPause')}>
|
<i class='icon play-pause' onclick=${() => dispatch('playPause')}>
|
||||||
${state.video.isPaused ? 'play_arrow' : 'pause'}
|
${state.video.isPaused ? 'play_arrow' : 'pause'}
|
||||||
|
|||||||
Reference in New Issue
Block a user