module.exports = Player var h = require('virtual-dom/h') var hyperx = require('hyperx') var hx = hyperx(h) // Shows a streaming video player. Standard features + Chromecast + Airplay 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`
dispatch('videoMouseMoved')}> ${showVideo ? renderVideo(state, dispatch) : renderCastScreen(state, dispatch)} ${renderPlayerControls(state, dispatch)}
` } function renderVideo (state, dispatch) { // Unfortunately, play/pause can't be done just by modifying HTML. // Instead, grab the DOM node and play/pause it if necessary var videoElement = document.querySelector('video') if (videoElement !== null) { if (state.video.isPaused && !videoElement.paused) { videoElement.pause() } else if (!state.video.isPaused && videoElement.paused) { videoElement.play() } // When the user clicks or drags on the progress bar, jump to that position if (state.video.jumpToTime) { videoElement.currentTime = state.video.jumpToTime state.video.jumpToTime = null } state.video.currentTime = videoElement.currentTime state.video.duration = videoElement.duration } return hx`
dispatch('videoMouseMoved')}>
` // As soon as the video loads enough to know the video dimensions, resize the window function onLoadedMetadata (e) { var video = e.target var dimensions = { width: video.videoWidth, height: video.videoHeight } dispatch('setDimensions', dimensions) } // When the video completes, pause the video instead of looping function onEnded (e) { state.video.isPaused = true } } 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') // Show a nice title image, if possible var style = {} var infoHash = state.playing.infoHash var torrentSummary = state.saved.torrents.find((x) => x.infoHash === infoHash) if (torrentSummary && torrentSummary.posterURL) { var cleanURL = torrentSummary.posterURL.replace(/\\/g, '/') style.backgroundImage = `radial-gradient(circle at center, rgba(0,0,0,0.4) 0%,rgba(0,0,0,1) 100%), url(${cleanURL})` } // Show whether we're connected to Chromecast / Airplay var castStatus = isStarting ? 'Connecting...' : 'Connected' return hx`
${isAirplay ? 'airplay' : 'cast'}
${isAirplay ? 'AirPlay' : 'Chromecast'}
${castStatus}
` } function renderPlayerControls (state, dispatch) { var positionPercent = 100 * state.video.currentTime / state.video.duration var playbackCursorStyle = { left: 'calc(' + positionPercent + '% - 8px)' } var elements = [ hx`
${renderLoadingBar(state)}
`, hx` dispatch('toggleFullScreen')}> ${state.window.isFullScreen ? 'fullscreen_exit' : 'fullscreen'} ` ] // 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') 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` cast `) } if (state.devices.airplay || isOnAirplay) { elements.push(hx` airplay `) } // 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 if (process.platform !== 'darwin') { elements.push(hx` dispatch('back')}> chevron_left `) } // Finally, the big button in the center plays or pauses the video elements.push(hx` dispatch('playPause')}> ${state.video.isPaused ? 'play_arrow' : 'pause'} `) return hx`
${elements}
` // Handles a click or drag to scrub (jump to another position in the video) function handleScrub (e) { dispatch('videoMouseMoved') var windowWidth = document.querySelector('body').clientWidth var fraction = e.clientX / windowWidth var position = fraction * state.video.duration /* seconds */ dispatch('playbackJump', position) } } // Renders the loading bar. Shows which parts of the torrent are loaded, which // can be "spongey" / non-contiguous function renderLoadingBar (state) { var torrent = state.client.get(state.playing.infoHash) if (torrent === null) { return [] } // Find all contiguous parts of the torrent which are loaded var parts = [] var lastPartPresent = false var numParts = torrent.pieces.length for (var i = 0; i < numParts; i++) { var partPresent = torrent.bitfield.get(i) if (partPresent && !lastPartPresent) { parts.push({start: i, count: 1}) } else if (partPresent) { parts[parts.length - 1].count++ } lastPartPresent = partPresent } // Output an list of rectangles to show loading progress return hx`
${parts.map(function (part) { var style = { left: (100 * part.start / numParts) + '%', width: (100 * part.count / numParts) + '%' } return hx`
` })}
` }