Initial decaf commit. Using bare pre-processed player.js instead of
decaffeinate.js
This commit is contained in:
parent
32dddab258
commit
f3306f2263
|
|
@ -1,41 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
var coffee = require('coffeescript');
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
var order = [
|
|
||||||
'base.coffee',
|
|
||||||
'vimeo.coffee',
|
|
||||||
'youtube.coffee',
|
|
||||||
'dailymotion.coffee',
|
|
||||||
'videojs.coffee',
|
|
||||||
'playerjs.coffee',
|
|
||||||
'streamable.coffee',
|
|
||||||
'gdrive-player.coffee',
|
|
||||||
'raw-file.coffee',
|
|
||||||
'soundcloud.coffee',
|
|
||||||
'embed.coffee',
|
|
||||||
'twitch.coffee',
|
|
||||||
'livestream.com.coffee',
|
|
||||||
'custom-embed.coffee',
|
|
||||||
'rtmp.coffee',
|
|
||||||
'smashcast.coffee',
|
|
||||||
'ustream.coffee',
|
|
||||||
'imgur.coffee',
|
|
||||||
'hls.coffee',
|
|
||||||
'twitchclip.coffee',
|
|
||||||
'update.coffee'
|
|
||||||
];
|
|
||||||
|
|
||||||
var buffer = '';
|
|
||||||
order.forEach(function (file) {
|
|
||||||
buffer += fs.readFileSync(
|
|
||||||
path.join(__dirname, '..', 'player', file)
|
|
||||||
) + '\n';
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(__dirname, '..', 'www', 'js', 'player.js'),
|
|
||||||
coffee.compile(buffer)
|
|
||||||
);
|
|
||||||
7091
package-lock.json
generated
7091
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -41,7 +41,6 @@
|
||||||
"yamljs": "^0.2.8"
|
"yamljs": "^0.2.8"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-player": "./bin/build-player.js",
|
|
||||||
"build-server": "babel -D --source-maps --out-dir lib/ src/",
|
"build-server": "babel -D --source-maps --out-dir lib/ src/",
|
||||||
"flow": "flow",
|
"flow": "flow",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
window.Player = class Player
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof Player)
|
|
||||||
return new Player(data)
|
|
||||||
|
|
||||||
@setMediaProperties(data)
|
|
||||||
@paused = false
|
|
||||||
@latched = true
|
|
||||||
@seeklatch = false #used to lock sync latch when seeking for sync
|
|
||||||
@lastSTime = 0
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
@setMediaProperties(data)
|
|
||||||
|
|
||||||
setMediaProperties: (data) ->
|
|
||||||
@mediaId = data.id
|
|
||||||
@mediaType = data.type
|
|
||||||
@mediaLength = data.seconds
|
|
||||||
|
|
||||||
play: ->
|
|
||||||
@paused = false
|
|
||||||
|
|
||||||
pause: ->
|
|
||||||
@paused = true
|
|
||||||
latch: ->
|
|
||||||
if not @latched
|
|
||||||
@latched = true
|
|
||||||
unlatch: ->
|
|
||||||
if not @seeklatch
|
|
||||||
if @latched
|
|
||||||
$("#latchvid").show()
|
|
||||||
@latched = false
|
|
||||||
else
|
|
||||||
@seeklatch = false
|
|
||||||
latchseek: ->
|
|
||||||
@seeklatch = true
|
|
||||||
getLatch: (cb) ->
|
|
||||||
cb(@latched)
|
|
||||||
seekTo: (time) ->
|
|
||||||
|
|
||||||
setVolume: (volume) ->
|
|
||||||
|
|
||||||
getTime: (cb) ->
|
|
||||||
cb(0)
|
|
||||||
|
|
||||||
isPaused: (cb) ->
|
|
||||||
cb(@paused)
|
|
||||||
|
|
||||||
getVolume: (cb) ->
|
|
||||||
cb(VOLUME)
|
|
||||||
|
|
||||||
getRes: (cb) ->
|
|
||||||
cb();
|
|
||||||
|
|
||||||
destroy: ->
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
CUSTOM_EMBED_WARNING = 'This channel is embedding custom content from %link%.
|
|
||||||
Since this content is not trusted, you must click "Embed" below to allow
|
|
||||||
the content to be embedded.<hr>'
|
|
||||||
|
|
||||||
window.CustomEmbedPlayer = class CustomEmbedPlayer extends EmbedPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof CustomEmbedPlayer)
|
|
||||||
return new CustomEmbedPlayer(data)
|
|
||||||
|
|
||||||
@load(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
if not data.meta.embed?
|
|
||||||
console.error('CustomEmbedPlayer::load(): missing meta.embed')
|
|
||||||
return
|
|
||||||
|
|
||||||
embedSrc = data.meta.embed.src
|
|
||||||
link = "<a href=\"#{embedSrc}\" target=\"_blank\"><strong>#{embedSrc}</strong></a>"
|
|
||||||
alert = makeAlert('Untrusted Content', CUSTOM_EMBED_WARNING.replace('%link%', link),
|
|
||||||
'alert-warning')
|
|
||||||
.removeClass('col-md-12')
|
|
||||||
$('<button/>').addClass('btn btn-default')
|
|
||||||
.text('Embed')
|
|
||||||
.click(=>
|
|
||||||
super(data)
|
|
||||||
)
|
|
||||||
.appendTo(alert.find('.alert'))
|
|
||||||
removeOld(alert)
|
|
||||||
|
|
@ -1,159 +0,0 @@
|
||||||
window.DailymotionPlayer = class DailymotionPlayer extends Player
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof DailymotionPlayer)
|
|
||||||
return new DailymotionPlayer(data)
|
|
||||||
|
|
||||||
@setMediaProperties(data)
|
|
||||||
@initialVolumeSet = false
|
|
||||||
@playbackReadyCb = null
|
|
||||||
@latched = true
|
|
||||||
|
|
||||||
waitUntilDefined(window, 'DM', =>
|
|
||||||
removeOld()
|
|
||||||
|
|
||||||
params =
|
|
||||||
autoplay: 1
|
|
||||||
wmode: if USEROPTS.wmode_transparent then 'transparent' else 'opaque'
|
|
||||||
logo: 0
|
|
||||||
|
|
||||||
quality = @mapQuality(USEROPTS.default_quality)
|
|
||||||
if quality != 'auto'
|
|
||||||
params.quality = quality
|
|
||||||
|
|
||||||
@dm = DM.player('ytapiplayer',
|
|
||||||
video: data.id
|
|
||||||
width: parseInt(VWIDTH, 10)
|
|
||||||
height: parseInt(VHEIGHT, 10)
|
|
||||||
params: params
|
|
||||||
)
|
|
||||||
|
|
||||||
@dm.addEventListener('apiready', =>
|
|
||||||
@dmReady = true
|
|
||||||
@dm.addEventListener('ended', ->
|
|
||||||
if CLIENT.leader
|
|
||||||
socket.emit('playNext')
|
|
||||||
)
|
|
||||||
|
|
||||||
@dm.addEventListener('pause', =>
|
|
||||||
@paused = true
|
|
||||||
setMini();
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
else
|
|
||||||
@unlatch()
|
|
||||||
)
|
|
||||||
|
|
||||||
@dm.addEventListener('playing', =>
|
|
||||||
@paused = false
|
|
||||||
setMini();
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
|
|
||||||
if not @initialVolumeSet
|
|
||||||
@setVolume(VOLUME)
|
|
||||||
@initialVolumeSet = true
|
|
||||||
)
|
|
||||||
|
|
||||||
@dm.addEventListener('seeked', =>
|
|
||||||
if not CLIENT.leader
|
|
||||||
@unlatch()
|
|
||||||
)
|
|
||||||
|
|
||||||
@dm.addEventListener('timeupdate', =>
|
|
||||||
setDur();
|
|
||||||
)
|
|
||||||
|
|
||||||
@dm.addEventListener('volumechange', =>
|
|
||||||
setMini();
|
|
||||||
)
|
|
||||||
|
|
||||||
# Once the video stops, the internal state of the player
|
|
||||||
# becomes unusable and attempting to load() will corrupt it and
|
|
||||||
# crash the player with an error. As a short–medium term
|
|
||||||
# workaround, mark the player as "not ready" until the next
|
|
||||||
# playback_ready event
|
|
||||||
@dm.addEventListener('video_end', =>
|
|
||||||
@dmReady = false
|
|
||||||
)
|
|
||||||
@dm.addEventListener('playback_ready', =>
|
|
||||||
@dmReady = true
|
|
||||||
handleWindowResize()
|
|
||||||
|
|
||||||
if @playbackReadyCb
|
|
||||||
@playbackReadyCb()
|
|
||||||
@playbackReadyCb = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
@setMediaProperties(data)
|
|
||||||
if @dm and @dmReady
|
|
||||||
@dm.load(data.id)
|
|
||||||
@dm.seek(data.currentTime)
|
|
||||||
|
|
||||||
else if @dm
|
|
||||||
# TODO: Player::load() needs to be made asynchronous in the future
|
|
||||||
console.log('Warning: load() called before DM is ready, queueing callback')
|
|
||||||
@playbackReadyCb = () =>
|
|
||||||
handleWindowResize()
|
|
||||||
|
|
||||||
@dm.load(data.id)
|
|
||||||
@dm.seek(data.currentTime)
|
|
||||||
else
|
|
||||||
console.error('WTF? DailymotionPlayer::load() called but @dm is undefined')
|
|
||||||
|
|
||||||
pause: ->
|
|
||||||
if @dm and @dmReady
|
|
||||||
@paused = true
|
|
||||||
@dm.pause()
|
|
||||||
|
|
||||||
play: ->
|
|
||||||
if @dm and @dmReady
|
|
||||||
@paused = false
|
|
||||||
@dm.play()
|
|
||||||
|
|
||||||
seekTo: (time) ->
|
|
||||||
if @dm and @dmReady
|
|
||||||
@dm.seek(time)
|
|
||||||
|
|
||||||
setVolume: (volume) ->
|
|
||||||
if @dm and @dmReady
|
|
||||||
@dm.setVolume(volume)
|
|
||||||
|
|
||||||
getTime: (cb) ->
|
|
||||||
if @dm and @dmReady
|
|
||||||
cb(@dm.currentTime)
|
|
||||||
else
|
|
||||||
cb(0)
|
|
||||||
|
|
||||||
getVolume: (cb) ->
|
|
||||||
if @dm and @dmReady
|
|
||||||
if @dm.muted
|
|
||||||
cb(0)
|
|
||||||
else
|
|
||||||
volume = @dm.volume
|
|
||||||
# There was once a bug in Dailymotion where it sometimes gave back
|
|
||||||
# volumes in the wrong range. Not sure if this is still a necessary
|
|
||||||
# check.
|
|
||||||
if volume > 1
|
|
||||||
volume /= 100
|
|
||||||
cb(volume)
|
|
||||||
else
|
|
||||||
cb(VOLUME)
|
|
||||||
|
|
||||||
getRes: (cb) ->
|
|
||||||
if @dm and @dmReady
|
|
||||||
cb([@dm.width, @dm.height])
|
|
||||||
else
|
|
||||||
cb()
|
|
||||||
mapQuality: (quality) ->
|
|
||||||
switch String(quality)
|
|
||||||
when '240', '480', '720', '1080' then String(quality)
|
|
||||||
when '360' then '380'
|
|
||||||
when 'best' then '1080'
|
|
||||||
else 'auto'
|
|
||||||
|
|
||||||
destroy: ->
|
|
||||||
if @dm
|
|
||||||
@dm.destroy('ytapiplayer')
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
DEFAULT_ERROR = 'You are currently connected via HTTPS but the embedded content
|
|
||||||
uses non-secure plain HTTP. Your browser therefore blocks it from
|
|
||||||
loading due to mixed content policy. To fix this, embed the video using a
|
|
||||||
secure link if available (https://...), or find another source for the content.'
|
|
||||||
|
|
||||||
genParam = (name, value) ->
|
|
||||||
$('<param/>').attr(
|
|
||||||
name: name
|
|
||||||
value: value
|
|
||||||
)
|
|
||||||
|
|
||||||
window.EmbedPlayer = class EmbedPlayer extends Player
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof EmbedPlayer)
|
|
||||||
return new EmbedPlayer(data)
|
|
||||||
|
|
||||||
@load(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
@setMediaProperties(data)
|
|
||||||
|
|
||||||
embed = data.meta.embed
|
|
||||||
if not embed?
|
|
||||||
console.error('EmbedPlayer::load(): missing meta.embed')
|
|
||||||
return
|
|
||||||
|
|
||||||
@player = @loadIframe(embed)
|
|
||||||
|
|
||||||
removeOld(@player)
|
|
||||||
|
|
||||||
loadIframe: (embed) ->
|
|
||||||
if embed.src.indexOf('http:') == 0 and location.protocol == 'https:'
|
|
||||||
if @__proto__.mixedContentError?
|
|
||||||
error = @__proto__.mixedContentError
|
|
||||||
else
|
|
||||||
error = DEFAULT_ERROR
|
|
||||||
alert = makeAlert('Mixed Content Error', error, 'alert-danger')
|
|
||||||
.removeClass('col-md-12')
|
|
||||||
alert.find('.close').remove()
|
|
||||||
return alert
|
|
||||||
else
|
|
||||||
iframe = $('<iframe/>').attr(
|
|
||||||
src: embed.src
|
|
||||||
frameborder: '0'
|
|
||||||
allow: 'autoplay'
|
|
||||||
allowfullscreen: '1'
|
|
||||||
)
|
|
||||||
|
|
||||||
return iframe
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
window.GoogleDrivePlayer = class GoogleDrivePlayer extends VideoJSPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof GoogleDrivePlayer)
|
|
||||||
return new GoogleDrivePlayer(data)
|
|
||||||
|
|
||||||
super(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
if not window.hasDriveUserscript
|
|
||||||
window.promptToInstallDriveUserscript()
|
|
||||||
else if window.hasDriveUserscript
|
|
||||||
window.maybePromptToUpgradeUserscript()
|
|
||||||
if typeof window.getGoogleDriveMetadata is 'function'
|
|
||||||
setTimeout(=>
|
|
||||||
backoffRetry((cb) ->
|
|
||||||
window.getGoogleDriveMetadata(data.id, cb)
|
|
||||||
, (error, metadata) =>
|
|
||||||
if error
|
|
||||||
console.error(error)
|
|
||||||
alertBox = window.document.createElement('div')
|
|
||||||
alertBox.className = 'alert alert-danger'
|
|
||||||
alertBox.textContent = error
|
|
||||||
document.getElementById('ytapiplayer').appendChild(alertBox)
|
|
||||||
else
|
|
||||||
data.meta.direct = metadata.videoMap
|
|
||||||
super(data)
|
|
||||||
, {
|
|
||||||
maxTries: 3
|
|
||||||
delay: 1000
|
|
||||||
factor: 1.2
|
|
||||||
jitter: 500
|
|
||||||
})
|
|
||||||
, Math.random() * 1000)
|
|
||||||
|
|
||||||
window.promptToInstallDriveUserscript = ->
|
|
||||||
if document.getElementById('prompt-install-drive-userscript')
|
|
||||||
return
|
|
||||||
alertBox = document.createElement('div')
|
|
||||||
alertBox.id = 'prompt-install-drive-userscript'
|
|
||||||
alertBox.className = 'alert alert-info'
|
|
||||||
alertBox.innerHTML = """
|
|
||||||
Due to continual breaking changes making it increasingly difficult to
|
|
||||||
maintain Google Drive support, Google Drive now requires installing
|
|
||||||
a userscript in order to play the video."""
|
|
||||||
alertBox.appendChild(document.createElement('br'))
|
|
||||||
infoLink = document.createElement('a')
|
|
||||||
infoLink.className = 'btn btn-info'
|
|
||||||
infoLink.href = '/google_drive_userscript'
|
|
||||||
infoLink.textContent = 'Click here for details'
|
|
||||||
infoLink.target = '_blank'
|
|
||||||
alertBox.appendChild(infoLink)
|
|
||||||
|
|
||||||
closeButton = document.createElement('button')
|
|
||||||
closeButton.className = 'close pull-right'
|
|
||||||
closeButton.innerHTML = '×'
|
|
||||||
closeButton.onclick = ->
|
|
||||||
alertBox.parentNode.removeChild(alertBox)
|
|
||||||
alertBox.insertBefore(closeButton, alertBox.firstChild)
|
|
||||||
removeOld($('<div/>').append(alertBox))
|
|
||||||
|
|
||||||
window.tellUserNotToContactMeAboutThingsThatAreNotSupported = ->
|
|
||||||
if document.getElementById('prompt-no-gdrive-support')
|
|
||||||
return
|
|
||||||
alertBox = document.createElement('div')
|
|
||||||
alertBox.id = 'prompt-no-gdrive-support'
|
|
||||||
alertBox.className = 'alert alert-danger'
|
|
||||||
alertBox.innerHTML = """
|
|
||||||
CyTube has detected an error in Google Drive playback. Please note that the
|
|
||||||
staff in CyTube support channels DO NOT PROVIDE SUPPORT FOR GOOGLE DRIVE. It
|
|
||||||
is left in the code as-is for existing users, but we will not assist in
|
|
||||||
troubleshooting any errors that occur.<br>"""
|
|
||||||
alertBox.appendChild(document.createElement('br'))
|
|
||||||
infoLink = document.createElement('a')
|
|
||||||
infoLink.className = 'btn btn-danger'
|
|
||||||
infoLink.href = 'https://github.com/calzoneman/sync/wiki/Frequently-Asked-Questions#why-dont-you-support-google-drive-anymore'
|
|
||||||
infoLink.textContent = 'Click here for details'
|
|
||||||
infoLink.target = '_blank'
|
|
||||||
alertBox.appendChild(infoLink)
|
|
||||||
|
|
||||||
closeButton = document.createElement('button')
|
|
||||||
closeButton.className = 'close pull-right'
|
|
||||||
closeButton.innerHTML = '×'
|
|
||||||
closeButton.onclick = ->
|
|
||||||
alertBox.parentNode.removeChild(alertBox)
|
|
||||||
alertBox.insertBefore(closeButton, alertBox.firstChild)
|
|
||||||
removeOld($('<div/>').append(alertBox))
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
window.HLSPlayer = class HLSPlayer extends VideoJSPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof HLSPlayer)
|
|
||||||
return new HLSPlayer(data)
|
|
||||||
|
|
||||||
@setupMeta(data)
|
|
||||||
super(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
@setupMeta(data)
|
|
||||||
super(data)
|
|
||||||
|
|
||||||
setupMeta: (data) ->
|
|
||||||
data.meta.direct =
|
|
||||||
# Quality is required for data.meta.direct processing but doesn't
|
|
||||||
# matter here because it's dictated by the stream. Arbitrarily
|
|
||||||
# choose 480.
|
|
||||||
480: [
|
|
||||||
{
|
|
||||||
link: data.id
|
|
||||||
contentType: 'application/x-mpegURL'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
window.ImgurPlayer = class ImgurPlayer extends EmbedPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof ImgurPlayer)
|
|
||||||
return new ImgurPlayer(data)
|
|
||||||
|
|
||||||
@load(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
data.meta.embed =
|
|
||||||
tag: 'iframe'
|
|
||||||
src: "https://imgur.com/a/#{data.id}/embed"
|
|
||||||
super(data)
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
window.LivestreamPlayer = class LivestreamPlayer extends EmbedPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof LivestreamPlayer)
|
|
||||||
return new LivestreamPlayer(data)
|
|
||||||
|
|
||||||
@load(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
data.meta.embed =
|
|
||||||
src: "https://cdn.livestream.com/embed/#{data.id}?\
|
|
||||||
layout=4&\
|
|
||||||
color=0x000000&\
|
|
||||||
iconColorOver=0xe7e7e7&\
|
|
||||||
iconColor=0xcccccc"
|
|
||||||
tag: 'iframe'
|
|
||||||
super(data)
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
window.PlayerJSPlayer = class PlayerJSPlayer extends Player
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof PlayerJSPlayer)
|
|
||||||
return new PlayerJSPlayer(data)
|
|
||||||
|
|
||||||
@load(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
@setMediaProperties(data)
|
|
||||||
@ready = false
|
|
||||||
@finishing = false
|
|
||||||
|
|
||||||
if not data.meta.playerjs
|
|
||||||
throw new Error('Invalid input: missing meta.playerjs')
|
|
||||||
|
|
||||||
waitUntilDefined(window, 'playerjs', =>
|
|
||||||
iframe = $('<iframe/>')
|
|
||||||
.attr(src: data.meta.playerjs.src)
|
|
||||||
|
|
||||||
removeOld(iframe)
|
|
||||||
|
|
||||||
@player = new playerjs.Player(iframe[0])
|
|
||||||
@player.on('ready', =>
|
|
||||||
@player.on('error', (error) =>
|
|
||||||
console.error('PlayerJS error', error.stack)
|
|
||||||
)
|
|
||||||
@player.on('ended', ->
|
|
||||||
# Streamable seems to not implement this since it loops
|
|
||||||
# gotta use the timeupdate hack below
|
|
||||||
if CLIENT.leader
|
|
||||||
socket.emit('playNext')
|
|
||||||
)
|
|
||||||
@player.on('timeupdate', (time) =>
|
|
||||||
if time.duration - time.seconds < 1 and not @finishing
|
|
||||||
setTimeout(=>
|
|
||||||
if CLIENT.leader
|
|
||||||
socket.emit('playNext')
|
|
||||||
@pause()
|
|
||||||
, (time.duration - time.seconds) * 1000)
|
|
||||||
@finishing = true
|
|
||||||
)
|
|
||||||
@player.on('play', ->
|
|
||||||
@paused = false
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
)
|
|
||||||
@player.on('pause', ->
|
|
||||||
@paused = true
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
)
|
|
||||||
|
|
||||||
@player.setVolume(VOLUME * 100)
|
|
||||||
|
|
||||||
if not @paused
|
|
||||||
@player.play()
|
|
||||||
|
|
||||||
@ready = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
play: ->
|
|
||||||
@paused = false
|
|
||||||
if @player and @ready
|
|
||||||
@player.play()
|
|
||||||
|
|
||||||
pause: ->
|
|
||||||
@paused = true
|
|
||||||
if @player and @ready
|
|
||||||
@player.pause()
|
|
||||||
|
|
||||||
seekTo: (time) ->
|
|
||||||
if @player and @ready
|
|
||||||
@player.setCurrentTime(time)
|
|
||||||
|
|
||||||
setVolume: (volume) ->
|
|
||||||
if @player and @ready
|
|
||||||
@player.setVolume(volume * 100)
|
|
||||||
|
|
||||||
getTime: (cb) ->
|
|
||||||
if @player and @ready
|
|
||||||
@player.getCurrentTime(cb)
|
|
||||||
else
|
|
||||||
cb(0)
|
|
||||||
|
|
||||||
getVolume: (cb) ->
|
|
||||||
if @player and @ready
|
|
||||||
@player.getVolume((volume) ->
|
|
||||||
cb(volume / 100)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
cb(VOLUME)
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
codecToMimeType = (codec) ->
|
|
||||||
switch codec
|
|
||||||
when 'mov/h264', 'mov/av1' then 'video/mp4'
|
|
||||||
when 'flv/h264' then 'video/flv'
|
|
||||||
when 'matroska/vp8', 'matroska/vp9', 'matroska/av1' then 'video/webm'
|
|
||||||
when 'ogg/theora' then 'video/ogg'
|
|
||||||
when 'mp3' then 'audio/mp3'
|
|
||||||
when 'vorbis' then 'audio/ogg'
|
|
||||||
when 'aac' then 'audio/aac'
|
|
||||||
when 'opus' then 'audio/opus'
|
|
||||||
else 'video/flv'
|
|
||||||
|
|
||||||
window.FilePlayer = class FilePlayer extends VideoJSPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof FilePlayer)
|
|
||||||
return new FilePlayer(data)
|
|
||||||
|
|
||||||
data.meta.direct =
|
|
||||||
480: [{
|
|
||||||
contentType: codecToMimeType(data.meta.codec)
|
|
||||||
link: data.id
|
|
||||||
}]
|
|
||||||
super(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
data.meta.direct =
|
|
||||||
480: [{
|
|
||||||
contentType: codecToMimeType(data.meta.codec)
|
|
||||||
link: data.id
|
|
||||||
}]
|
|
||||||
super(data)
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
window.RTMPPlayer = class RTMPPlayer extends VideoJSPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof RTMPPlayer)
|
|
||||||
return new RTMPPlayer(data)
|
|
||||||
|
|
||||||
@setupMeta(data)
|
|
||||||
super(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
@setupMeta(data)
|
|
||||||
super(data)
|
|
||||||
|
|
||||||
setupMeta: (data) ->
|
|
||||||
data.meta.direct =
|
|
||||||
# Quality is required for data.meta.direct processing but doesn't
|
|
||||||
# matter here because it's dictated by the stream. Arbitrarily
|
|
||||||
# choose 480.
|
|
||||||
480: [
|
|
||||||
{
|
|
||||||
link: data.id
|
|
||||||
contentType: 'rtmp/flv'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
window.SmashcastPlayer = class SmashcastPlayer extends EmbedPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof SmashcastPlayer)
|
|
||||||
return new SmashcastPlayer(data)
|
|
||||||
|
|
||||||
@load(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
data.meta.embed =
|
|
||||||
src: "https://www.smashcast.tv/embed/#{data.id}"
|
|
||||||
tag: 'iframe'
|
|
||||||
super(data)
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
window.SoundCloudPlayer = class SoundCloudPlayer extends Player
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof SoundCloudPlayer)
|
|
||||||
return new SoundCloudPlayer(data)
|
|
||||||
|
|
||||||
@setMediaProperties(data)
|
|
||||||
|
|
||||||
waitUntilDefined(window, 'SC', =>
|
|
||||||
removeOld()
|
|
||||||
|
|
||||||
# For tracks that are private, but embeddable, the API returns a
|
|
||||||
# special URL to load into the player.
|
|
||||||
# TODO: rename scuri?
|
|
||||||
if data.meta.scuri
|
|
||||||
soundUrl = data.meta.scuri
|
|
||||||
else
|
|
||||||
soundUrl = data.id
|
|
||||||
|
|
||||||
widget = $('<iframe/>').appendTo($('#ytapiplayer'))
|
|
||||||
widget.attr(
|
|
||||||
id: 'scplayer'
|
|
||||||
src: "https://w.soundcloud.com/player/?url=#{soundUrl}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Soundcloud embed widget doesn't have a volume control.
|
|
||||||
sliderHolder = $('<div/>').attr('id', 'soundcloud-volume-holder')
|
|
||||||
.insertAfter(widget)
|
|
||||||
$('<span/>').attr('id', 'soundcloud-volume-label')
|
|
||||||
.addClass('label label-default')
|
|
||||||
.text('Volume')
|
|
||||||
.appendTo(sliderHolder)
|
|
||||||
volumeSlider = $('<div/>').attr('id', 'soundcloud-volume')
|
|
||||||
.appendTo(sliderHolder)
|
|
||||||
.slider(
|
|
||||||
range: 'min'
|
|
||||||
value: VOLUME * 100
|
|
||||||
stop: (event, ui) =>
|
|
||||||
@setVolume(ui.value / 100)
|
|
||||||
)
|
|
||||||
|
|
||||||
@soundcloud = SC.Widget(widget[0])
|
|
||||||
@soundcloud.bind(SC.Widget.Events.READY, =>
|
|
||||||
@soundcloud.ready = true
|
|
||||||
@setVolume(VOLUME)
|
|
||||||
@play()
|
|
||||||
|
|
||||||
@soundcloud.bind(SC.Widget.Events.PAUSE, =>
|
|
||||||
@paused = true
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
)
|
|
||||||
@soundcloud.bind(SC.Widget.Events.PLAY, =>
|
|
||||||
@paused = false
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
)
|
|
||||||
@soundcloud.bind(SC.Widget.Events.FINISH, =>
|
|
||||||
if CLIENT.leader
|
|
||||||
socket.emit('playNext')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
@setMediaProperties(data)
|
|
||||||
if @soundcloud and @soundcloud.ready
|
|
||||||
if data.meta.scuri
|
|
||||||
soundUrl = data.meta.scuri
|
|
||||||
else
|
|
||||||
soundUrl = data.id
|
|
||||||
@soundcloud.load(soundUrl, auto_play: true)
|
|
||||||
@soundcloud.bind(SC.Widget.Events.READY, =>
|
|
||||||
@setVolume(VOLUME)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
console.error('SoundCloudPlayer::load() called but soundcloud is not ready')
|
|
||||||
|
|
||||||
play: ->
|
|
||||||
@paused = false
|
|
||||||
if @soundcloud and @soundcloud.ready
|
|
||||||
@soundcloud.play()
|
|
||||||
|
|
||||||
pause: ->
|
|
||||||
@paused = true
|
|
||||||
if @soundcloud and @soundcloud.ready
|
|
||||||
@soundcloud.pause()
|
|
||||||
|
|
||||||
seekTo: (time) ->
|
|
||||||
if @soundcloud and @soundcloud.ready
|
|
||||||
# SoundCloud measures time in milliseconds while CyTube uses seconds.
|
|
||||||
@soundcloud.seekTo(time * 1000)
|
|
||||||
|
|
||||||
setVolume: (volume) ->
|
|
||||||
if @soundcloud and @soundcloud.ready
|
|
||||||
@soundcloud.setVolume(volume * 100)
|
|
||||||
|
|
||||||
getTime: (cb) ->
|
|
||||||
if @soundcloud and @soundcloud.ready
|
|
||||||
# Returned time is in milliseconds; CyTube expects seconds
|
|
||||||
@soundcloud.getPosition((time) -> cb(time / 1000))
|
|
||||||
else
|
|
||||||
cb(0)
|
|
||||||
|
|
||||||
getVolume: (cb) ->
|
|
||||||
if @soundcloud and @soundcloud.ready
|
|
||||||
@soundcloud.getVolume((vol) -> cb(vol / 100))
|
|
||||||
else
|
|
||||||
cb(VOLUME)
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
window.StreamablePlayer = class StreamablePlayer extends PlayerJSPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof StreamablePlayer)
|
|
||||||
return new StreamablePlayer(data)
|
|
||||||
|
|
||||||
super(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
data.meta.playerjs =
|
|
||||||
src: "https://streamable.com/e/#{data.id}"
|
|
||||||
|
|
||||||
super(data)
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
window.TWITCH_PARAMS_ERROR = 'The Twitch embed player now uses parameters which only
|
|
||||||
work if the following requirements are met: (1) The embedding website uses
|
|
||||||
HTTPS; (2) The embedding website uses the default port (443) and is accessed
|
|
||||||
via https://example.com instead of https://example.com:port. I have no
|
|
||||||
control over this -- see <a href="https://discuss.dev.twitch.tv/t/twitch-embedded-player-migration-timeline-update/25588" rel="noopener noreferrer" target="_blank">this Twitch post</a>
|
|
||||||
for details'
|
|
||||||
|
|
||||||
window.TwitchPlayer = class TwitchPlayer extends Player
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof TwitchPlayer)
|
|
||||||
return new TwitchPlayer(data)
|
|
||||||
|
|
||||||
@setMediaProperties(data)
|
|
||||||
waitUntilDefined(window, 'Twitch', =>
|
|
||||||
waitUntilDefined(Twitch, 'Player', =>
|
|
||||||
@init(data)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
init: (data) ->
|
|
||||||
removeOld()
|
|
||||||
|
|
||||||
if location.hostname != location.host or location.protocol != 'https:'
|
|
||||||
alert = makeAlert(
|
|
||||||
'Twitch API Parameters',
|
|
||||||
window.TWITCH_PARAMS_ERROR,
|
|
||||||
'alert-danger'
|
|
||||||
).removeClass('col-md-12')
|
|
||||||
removeOld(alert)
|
|
||||||
@twitch = null
|
|
||||||
return
|
|
||||||
|
|
||||||
options =
|
|
||||||
parent: [location.hostname]
|
|
||||||
width: $('#ytapiplayer').width()
|
|
||||||
height: $('#ytapiplayer').height()
|
|
||||||
|
|
||||||
if data.type is 'tv'
|
|
||||||
# VOD
|
|
||||||
options.video = data.id
|
|
||||||
else
|
|
||||||
# Livestream
|
|
||||||
options.channel = data.id
|
|
||||||
|
|
||||||
@twitch = new Twitch.Player('ytapiplayer', options)
|
|
||||||
@twitch.addEventListener(Twitch.Player.READY, =>
|
|
||||||
@setVolume(VOLUME)
|
|
||||||
@twitch.setQuality(@mapQuality(USEROPTS.default_quality))
|
|
||||||
@twitch.addEventListener(Twitch.Player.PLAY, =>
|
|
||||||
@paused = false
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
)
|
|
||||||
@twitch.addEventListener(Twitch.Player.PAUSE, =>
|
|
||||||
@paused = true
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
)
|
|
||||||
@twitch.addEventListener(Twitch.Player.ENDED, =>
|
|
||||||
if CLIENT.leader
|
|
||||||
socket.emit('playNext')
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
@setMediaProperties(data)
|
|
||||||
try
|
|
||||||
if data.type is 'tv'
|
|
||||||
# VOD
|
|
||||||
@twitch.setVideo(data.id)
|
|
||||||
else
|
|
||||||
# Livestream
|
|
||||||
@twitch.setChannel(data.id)
|
|
||||||
catch error
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
pause: ->
|
|
||||||
try
|
|
||||||
@twitch.pause()
|
|
||||||
@paused = true
|
|
||||||
catch error
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
play: ->
|
|
||||||
try
|
|
||||||
@twitch.play()
|
|
||||||
@paused = false
|
|
||||||
catch error
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
seekTo: (time) ->
|
|
||||||
try
|
|
||||||
@twitch.seek(time)
|
|
||||||
catch error
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
getTime: (cb) ->
|
|
||||||
try
|
|
||||||
cb(@twitch.getCurrentTime())
|
|
||||||
catch error
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
setVolume: (volume) ->
|
|
||||||
try
|
|
||||||
@twitch.setVolume(volume)
|
|
||||||
if volume > 0
|
|
||||||
@twitch.setMuted(false)
|
|
||||||
catch error
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
getVolume: (cb) ->
|
|
||||||
try
|
|
||||||
if @twitch.isPaused()
|
|
||||||
cb(0)
|
|
||||||
else
|
|
||||||
cb(@twitch.getVolume())
|
|
||||||
catch error
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
mapQuality: (quality) ->
|
|
||||||
switch String(quality)
|
|
||||||
when '1080' then 'chunked'
|
|
||||||
when '720' then 'high'
|
|
||||||
when '480' then 'medium'
|
|
||||||
when '360' then 'low'
|
|
||||||
when '240' then 'mobile'
|
|
||||||
when 'best' then 'chunked'
|
|
||||||
else ''
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
window.TwitchClipPlayer = class TwitchClipPlayer extends EmbedPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof TwitchClipPlayer)
|
|
||||||
return new TwitchClipPlayer(data)
|
|
||||||
|
|
||||||
@load(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
if location.hostname != location.host or location.protocol != 'https:'
|
|
||||||
alert = makeAlert(
|
|
||||||
'Twitch API Parameters',
|
|
||||||
window.TWITCH_PARAMS_ERROR,
|
|
||||||
'alert-danger'
|
|
||||||
).removeClass('col-md-12')
|
|
||||||
removeOld(alert)
|
|
||||||
return
|
|
||||||
|
|
||||||
data.meta.embed =
|
|
||||||
tag: 'iframe'
|
|
||||||
src: "https://clips.twitch.tv/embed?clip=#{data.id}&parent=#{location.host}"
|
|
||||||
super(data)
|
|
||||||
|
|
@ -1,127 +0,0 @@
|
||||||
TYPE_MAP =
|
|
||||||
yt: YouTubePlayer
|
|
||||||
vi: VimeoPlayer
|
|
||||||
dm: DailymotionPlayer
|
|
||||||
gd: GoogleDrivePlayer
|
|
||||||
gp: VideoJSPlayer
|
|
||||||
fi: FilePlayer
|
|
||||||
sc: SoundCloudPlayer
|
|
||||||
li: LivestreamPlayer
|
|
||||||
tw: TwitchPlayer
|
|
||||||
tv: TwitchPlayer
|
|
||||||
cu: CustomEmbedPlayer
|
|
||||||
rt: RTMPPlayer
|
|
||||||
hb: SmashcastPlayer
|
|
||||||
us: UstreamPlayer
|
|
||||||
im: ImgurPlayer
|
|
||||||
hl: HLSPlayer
|
|
||||||
sb: StreamablePlayer
|
|
||||||
tc: TwitchClipPlayer
|
|
||||||
cm: VideoJSPlayer
|
|
||||||
|
|
||||||
window.loadMediaPlayer = (data) ->
|
|
||||||
try
|
|
||||||
if window.PLAYER
|
|
||||||
window.PLAYER.destroy()
|
|
||||||
catch error
|
|
||||||
console.error error
|
|
||||||
|
|
||||||
if data.meta.direct and data.type is 'vi'
|
|
||||||
try
|
|
||||||
window.PLAYER = new VideoJSPlayer(data)
|
|
||||||
catch e
|
|
||||||
console.error e
|
|
||||||
else if data.type of TYPE_MAP
|
|
||||||
try
|
|
||||||
window.PLAYER = TYPE_MAP[data.type](data)
|
|
||||||
catch e
|
|
||||||
console.error e
|
|
||||||
|
|
||||||
window.handleMediaUpdate = (data) ->
|
|
||||||
PLAYER = window.PLAYER
|
|
||||||
|
|
||||||
|
|
||||||
#update airdate
|
|
||||||
dispSTimes();
|
|
||||||
|
|
||||||
PLAYER.lastSTime = data.currentTime;
|
|
||||||
if not PLAYER.latched #check if the shits latched, if not stop while we're ahead.
|
|
||||||
return
|
|
||||||
|
|
||||||
# Do not update if the current time is past the end of the video, unless
|
|
||||||
# the video has length 0 (which is a special case for livestreams)
|
|
||||||
if (typeof PLAYER.mediaLength is 'number' and
|
|
||||||
PLAYER.mediaLength > 0 and
|
|
||||||
data.currentTime > PLAYER.mediaLength)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Negative currentTime indicates a lead-in for clients to load the video,
|
|
||||||
# but not play it yet (helps with initial buffering)
|
|
||||||
waiting = data.currentTime < 0
|
|
||||||
|
|
||||||
# Load a new video in the same player if the ID changed
|
|
||||||
if data.id and data.id != PLAYER.mediaId
|
|
||||||
if data.currentTime < 0
|
|
||||||
data.currentTime = 0
|
|
||||||
PLAYER.load(data)
|
|
||||||
PLAYER.play()
|
|
||||||
|
|
||||||
if waiting
|
|
||||||
PLAYER.seekTo(0)
|
|
||||||
PLAYER.latchseek()
|
|
||||||
# YouTube player has a race condition that crashes the player if
|
|
||||||
# play(), seek(0), and pause() are called quickly without waiting
|
|
||||||
# for events to fire. Setting a flag variable that is checked in the
|
|
||||||
# event handler mitigates this.
|
|
||||||
if PLAYER instanceof YouTubePlayer
|
|
||||||
PLAYER.pauseSeekRaceCondition = true
|
|
||||||
else
|
|
||||||
PLAYER.pause()
|
|
||||||
return
|
|
||||||
else if PLAYER instanceof YouTubePlayer
|
|
||||||
PLAYER.pauseSeekRaceCondition = false
|
|
||||||
|
|
||||||
if CLIENT.leader or not USEROPTS.synch
|
|
||||||
return
|
|
||||||
|
|
||||||
if data.paused and not PLAYER.paused
|
|
||||||
PLAYER.seekTo(data.currentTime)
|
|
||||||
PLAYER.latchseek()
|
|
||||||
PLAYER.pause()
|
|
||||||
else if PLAYER.paused and not data.paused
|
|
||||||
PLAYER.play()
|
|
||||||
|
|
||||||
PLAYER.getTime((seconds) ->
|
|
||||||
time = data.currentTime
|
|
||||||
diff = (time - seconds) or time
|
|
||||||
accuracy = USEROPTS.sync_accuracy
|
|
||||||
|
|
||||||
# Dailymotion can't seek very accurately in Flash due to keyframe
|
|
||||||
# placement. Accuracy should not be set lower than 5 or the video
|
|
||||||
# may be very choppy.
|
|
||||||
if PLAYER instanceof DailymotionPlayer
|
|
||||||
accuracy = Math.max(accuracy, 5)
|
|
||||||
|
|
||||||
if diff > accuracy
|
|
||||||
# The player is behind the correct time
|
|
||||||
PLAYER.seekTo(time)
|
|
||||||
PLAYER.latchseek()
|
|
||||||
else if diff < -accuracy
|
|
||||||
# The player is ahead of the correct time
|
|
||||||
# Don't seek all the way back, to account for possible buffering.
|
|
||||||
# However, do seek all the way back for Dailymotion due to the
|
|
||||||
# keyframe issue mentioned above.
|
|
||||||
if not (PLAYER instanceof DailymotionPlayer)
|
|
||||||
time += 1
|
|
||||||
PLAYER.seekTo(time)
|
|
||||||
PLAYER.latchseek()
|
|
||||||
)
|
|
||||||
|
|
||||||
window.removeOld = (replace) ->
|
|
||||||
$('#soundcloud-volume-holder').remove()
|
|
||||||
replace ?= $('<div/>').addClass('embed-responsive-item')
|
|
||||||
old = $('#ytapiplayer')
|
|
||||||
replace.insertBefore(old)
|
|
||||||
old.remove()
|
|
||||||
replace.attr('id', 'ytapiplayer')
|
|
||||||
return replace
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
window.UstreamPlayer = class UstreamPlayer extends EmbedPlayer
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof UstreamPlayer)
|
|
||||||
return new UstreamPlayer(data)
|
|
||||||
|
|
||||||
@load(data)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
data.meta.embed =
|
|
||||||
tag: 'iframe'
|
|
||||||
src: "https://www.ustream.tv/embed/#{data.id}?html5ui"
|
|
||||||
super(data)
|
|
||||||
|
|
@ -1,269 +0,0 @@
|
||||||
sortSources = (sources) ->
|
|
||||||
if not sources
|
|
||||||
console.error('sortSources() called with null source list')
|
|
||||||
return []
|
|
||||||
|
|
||||||
qualities = ['2160', '1440', '1080', '720', '540', '480', '360', '240']
|
|
||||||
pref = String(USEROPTS.default_quality)
|
|
||||||
if USEROPTS.default_quality == 'best'
|
|
||||||
pref = '2160'
|
|
||||||
idx = qualities.indexOf(pref)
|
|
||||||
if idx < 0
|
|
||||||
idx = 5 # 480p
|
|
||||||
|
|
||||||
qualityOrder = qualities.slice(idx).concat(qualities.slice(0, idx).reverse())
|
|
||||||
qualityOrder.unshift('auto')
|
|
||||||
sourceOrder = []
|
|
||||||
flvOrder = []
|
|
||||||
for quality in qualityOrder
|
|
||||||
if quality of sources
|
|
||||||
flv = []
|
|
||||||
nonflv = []
|
|
||||||
sources[quality].forEach((source) ->
|
|
||||||
source.quality = quality
|
|
||||||
if source.contentType == 'video/flv'
|
|
||||||
flv.push(source)
|
|
||||||
else
|
|
||||||
nonflv.push(source)
|
|
||||||
)
|
|
||||||
sourceOrder = sourceOrder.concat(nonflv)
|
|
||||||
flvOrder = flvOrder.concat(flv)
|
|
||||||
|
|
||||||
return sourceOrder.concat(flvOrder).map((source) ->
|
|
||||||
type: source.contentType
|
|
||||||
src: source.link
|
|
||||||
res: source.quality
|
|
||||||
label: getSourceLabel(source)
|
|
||||||
)
|
|
||||||
|
|
||||||
getSourceLabel = (source) ->
|
|
||||||
if source.res is 'auto'
|
|
||||||
return 'auto'
|
|
||||||
else
|
|
||||||
return "#{source.quality}p #{source.contentType.split('/')[1]}"
|
|
||||||
|
|
||||||
waitUntilDefined(window, 'videojs', =>
|
|
||||||
videojs.options.flash.swf = '/video-js.swf'
|
|
||||||
)
|
|
||||||
|
|
||||||
hasAnyTextTracks = (data) ->
|
|
||||||
ntracks = data?.meta?.textTracks?.length ? 0
|
|
||||||
return ntracks > 0
|
|
||||||
|
|
||||||
window.VideoJSPlayer = class VideoJSPlayer extends Player
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof VideoJSPlayer)
|
|
||||||
return new VideoJSPlayer(data)
|
|
||||||
@load(data)
|
|
||||||
|
|
||||||
loadPlayer: (data) ->
|
|
||||||
waitUntilDefined(window, 'videojs', =>
|
|
||||||
attrs =
|
|
||||||
width: '100%'
|
|
||||||
height: '100%'
|
|
||||||
|
|
||||||
if @mediaType == 'cm' and hasAnyTextTracks(data)
|
|
||||||
attrs.crossorigin = 'anonymous'
|
|
||||||
|
|
||||||
video = $('<video/>')
|
|
||||||
.addClass('video-js vjs-default-skin embed-responsive-item')
|
|
||||||
.attr(attrs)
|
|
||||||
removeOld(video)
|
|
||||||
|
|
||||||
@sources = sortSources(data.meta.direct)
|
|
||||||
if @sources.length == 0
|
|
||||||
console.error('VideoJSPlayer::constructor(): data.meta.direct
|
|
||||||
has no sources!')
|
|
||||||
@mediaType = null
|
|
||||||
return
|
|
||||||
|
|
||||||
@sourceIdx = 0
|
|
||||||
|
|
||||||
# TODO: Refactor VideoJSPlayer to use a preLoad()/load()/postLoad() pattern
|
|
||||||
# VideoJSPlayer should provide the core functionality and logic for specific
|
|
||||||
# dependent player types (gdrive) should be an extension
|
|
||||||
if data.meta.gdrive_subtitles
|
|
||||||
data.meta.gdrive_subtitles.available.forEach((subt) ->
|
|
||||||
label = subt.lang_original
|
|
||||||
if subt.name
|
|
||||||
label += " (#{subt.name})"
|
|
||||||
$('<track/>').attr(
|
|
||||||
src: "/gdvtt/#{data.id}/#{subt.lang}/#{subt.name}.vtt?\
|
|
||||||
vid=#{data.meta.gdrive_subtitles.vid}"
|
|
||||||
kind: 'subtitles'
|
|
||||||
srclang: subt.lang
|
|
||||||
label: label
|
|
||||||
).appendTo(video)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@player = videojs(video[0],
|
|
||||||
# https://github.com/Dash-Industry-Forum/dash.js/issues/2184
|
|
||||||
autoplay: @sources[0].type != 'application/dash+xml',
|
|
||||||
controls: true,
|
|
||||||
plugins:
|
|
||||||
videoJsResolutionSwitcher:
|
|
||||||
default: @sources[0].res
|
|
||||||
)
|
|
||||||
@player.ready(=>
|
|
||||||
# Have to use updateSrc instead of <source> tags
|
|
||||||
# see: https://github.com/videojs/video.js/issues/3428
|
|
||||||
@player.updateSrc(@sources)
|
|
||||||
@player.on('error', =>
|
|
||||||
err = @player.error()
|
|
||||||
if err and err.code == 4
|
|
||||||
console.error('Caught error, trying next source')
|
|
||||||
# Does this really need to be done manually?
|
|
||||||
@sourceIdx++
|
|
||||||
if @sourceIdx < @sources.length
|
|
||||||
@player.src(@sources[@sourceIdx])
|
|
||||||
else
|
|
||||||
console.error('Out of sources, video will not play')
|
|
||||||
if @mediaType is 'gd'
|
|
||||||
if not window.hasDriveUserscript
|
|
||||||
window.promptToInstallDriveUserscript()
|
|
||||||
else
|
|
||||||
window.tellUserNotToContactMeAboutThingsThatAreNotSupported()
|
|
||||||
)
|
|
||||||
@setVolume(VOLUME)
|
|
||||||
@player.on('ended', ->
|
|
||||||
if CLIENT.leader
|
|
||||||
socket.emit('playNext')
|
|
||||||
)
|
|
||||||
|
|
||||||
@player.on('pause', =>
|
|
||||||
@paused = true
|
|
||||||
setMini();
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
else
|
|
||||||
@unlatch()
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
@player.on('play', =>
|
|
||||||
@paused = false
|
|
||||||
setMini();
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
)
|
|
||||||
|
|
||||||
@player.on('volumechange', =>
|
|
||||||
setMini();
|
|
||||||
)
|
|
||||||
|
|
||||||
# Workaround for IE-- even after seeking completes, the loading
|
|
||||||
# spinner remains.
|
|
||||||
@player.on('seeked', =>
|
|
||||||
$('.vjs-waiting').removeClass('vjs-waiting')
|
|
||||||
if not CLIENT.leader #this part has nothing to do with IE and all to do with sync latching :P
|
|
||||||
@unlatch()
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@player.on('canplay', =>
|
|
||||||
handleWindowResize()
|
|
||||||
console.log(@player.children)
|
|
||||||
)
|
|
||||||
|
|
||||||
@player.on('timeupdate', =>
|
|
||||||
setDur();
|
|
||||||
)
|
|
||||||
|
|
||||||
# Workaround for Chrome-- it seems that the click bindings for
|
|
||||||
# the subtitle menu aren't quite set up until after the ready
|
|
||||||
# event finishes, so set a timeout for 1ms to force this code
|
|
||||||
# not to run until the ready() function returns.
|
|
||||||
setTimeout(->
|
|
||||||
$('#ytapiplayer .vjs-subtitles-button .vjs-menu-item').each((i, elem) ->
|
|
||||||
textNode = elem.childNodes[0]
|
|
||||||
if textNode.textContent == localStorage.lastSubtitle
|
|
||||||
elem.click()
|
|
||||||
|
|
||||||
elem.onclick = ->
|
|
||||||
if elem.attributes['aria-checked'].value == 'true'
|
|
||||||
localStorage.lastSubtitle = textNode.textContent
|
|
||||||
)
|
|
||||||
, 1)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
@setMediaProperties(data)
|
|
||||||
@latched = true
|
|
||||||
# Note: VideoJS does have facilities for loading new videos into the
|
|
||||||
# existing player object, however it appears to be pretty glitchy when
|
|
||||||
# a video can't be played (either previous or next video). It's safer
|
|
||||||
# to just reset the entire thing.
|
|
||||||
@destroy()
|
|
||||||
@loadPlayer(data)
|
|
||||||
@setTracks(data)
|
|
||||||
|
|
||||||
setTracks: (data) ->
|
|
||||||
if data.meta.textTracks
|
|
||||||
data.meta.textTracks.forEach((track) ->
|
|
||||||
label = track.name
|
|
||||||
$('<track>').attr(
|
|
||||||
src: track.url
|
|
||||||
kind: 'subtitles'
|
|
||||||
type: track.type
|
|
||||||
label: label
|
|
||||||
default: true
|
|
||||||
).prependTo("video")
|
|
||||||
)
|
|
||||||
|
|
||||||
play: ->
|
|
||||||
@paused = false
|
|
||||||
if @player and @player.readyState() > 0
|
|
||||||
@player.play()
|
|
||||||
|
|
||||||
pause: ->
|
|
||||||
@paused = true
|
|
||||||
if @player and @player.readyState() > 0
|
|
||||||
@player.pause()
|
|
||||||
if not CLIENT.leader
|
|
||||||
@unlatch()
|
|
||||||
|
|
||||||
|
|
||||||
seekTo: (time) ->
|
|
||||||
if @player and @player.readyState() > 0
|
|
||||||
@player.currentTime(time)
|
|
||||||
|
|
||||||
setVolume: (volume) ->
|
|
||||||
if @player
|
|
||||||
@player.volume(volume)
|
|
||||||
|
|
||||||
getTime: (cb) ->
|
|
||||||
if @player and @player.readyState() > 0
|
|
||||||
cb(@player.currentTime())
|
|
||||||
else
|
|
||||||
cb(0)
|
|
||||||
|
|
||||||
getVolume: (cb) ->
|
|
||||||
if @player and @player.readyState() > 0
|
|
||||||
if @player.muted()
|
|
||||||
cb(0)
|
|
||||||
else
|
|
||||||
cb(@player.volume())
|
|
||||||
else
|
|
||||||
cb(VOLUME)
|
|
||||||
|
|
||||||
getRes: (cb) ->
|
|
||||||
if @player
|
|
||||||
cb([@player.videoWidth(), @player.videoHeight()])
|
|
||||||
else
|
|
||||||
cb()
|
|
||||||
|
|
||||||
destroy: ->
|
|
||||||
removeOld()
|
|
||||||
if @player
|
|
||||||
@player.dispose()
|
|
||||||
|
|
@ -1,133 +0,0 @@
|
||||||
window.VimeoPlayer = class VimeoPlayer extends Player
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof VimeoPlayer)
|
|
||||||
return new VimeoPlayer(data)
|
|
||||||
|
|
||||||
@load(data)
|
|
||||||
@resx = 0
|
|
||||||
@resy = 0
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
@setMediaProperties(data)
|
|
||||||
@latched = true
|
|
||||||
|
|
||||||
waitUntilDefined(window, 'Vimeo', =>
|
|
||||||
video = $('<iframe/>')
|
|
||||||
removeOld(video)
|
|
||||||
video.attr(
|
|
||||||
src: "https://player.vimeo.com/video/#{data.id}"
|
|
||||||
webkitallowfullscreen: true
|
|
||||||
mozallowfullscreen: true
|
|
||||||
allowfullscreen: true
|
|
||||||
)
|
|
||||||
|
|
||||||
if USEROPTS.wmode_transparent
|
|
||||||
video.attr('wmode', 'transparent')
|
|
||||||
|
|
||||||
@vimeo = new Vimeo.Player(video[0])
|
|
||||||
|
|
||||||
@vimeo.on('ended', =>
|
|
||||||
if CLIENT.leader
|
|
||||||
socket.emit('playNext')
|
|
||||||
)
|
|
||||||
|
|
||||||
@vimeo.on('pause', =>
|
|
||||||
@paused = true
|
|
||||||
setMini();
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
else
|
|
||||||
@unlatch()
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
@vimeo.on('play', =>
|
|
||||||
@paused = false
|
|
||||||
setMini();
|
|
||||||
handleWindowResize()
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
)
|
|
||||||
|
|
||||||
@vimeo.on('seeked', =>
|
|
||||||
if not CLIENT.leader
|
|
||||||
@unlatch()
|
|
||||||
)
|
|
||||||
|
|
||||||
@vimeo.on('timeupdate', =>
|
|
||||||
setDur();
|
|
||||||
)
|
|
||||||
|
|
||||||
@vimeo.on('volumechange', =>
|
|
||||||
setMini();
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@play()
|
|
||||||
@setVolume(VOLUME)
|
|
||||||
|
|
||||||
@vimeo.getVideoWidth().then((wid) ->
|
|
||||||
window.PLAYER.resx = wid
|
|
||||||
).catch((error) ->
|
|
||||||
console.error('vimeo::getVideoWidth():', error)
|
|
||||||
)
|
|
||||||
|
|
||||||
@vimeo.getVideoHeight().then((hgt) ->
|
|
||||||
window.PLAYER.resy = hgt
|
|
||||||
).catch((error) ->
|
|
||||||
console.error('vimeo::getVideoHeight():', error)
|
|
||||||
)
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
play: ->
|
|
||||||
@paused = false
|
|
||||||
if @vimeo
|
|
||||||
@vimeo.play().catch((error) ->
|
|
||||||
console.error('vimeo::play():', error)
|
|
||||||
)
|
|
||||||
|
|
||||||
pause: ->
|
|
||||||
@paused = true
|
|
||||||
if @vimeo
|
|
||||||
@vimeo.pause().catch((error) ->
|
|
||||||
console.error('vimeo::pause():', error)
|
|
||||||
)
|
|
||||||
|
|
||||||
seekTo: (time) ->
|
|
||||||
if @vimeo
|
|
||||||
@vimeo.setCurrentTime(time).catch((error) ->
|
|
||||||
console.error('vimeo::setCurrentTime():', error)
|
|
||||||
)
|
|
||||||
|
|
||||||
setVolume: (volume) ->
|
|
||||||
if @vimeo
|
|
||||||
@vimeo.setVolume(volume).catch((error) ->
|
|
||||||
console.error('vimeo::setVolume():', error)
|
|
||||||
)
|
|
||||||
|
|
||||||
getTime: (cb) ->
|
|
||||||
if @vimeo
|
|
||||||
@vimeo.getCurrentTime().then((time) ->
|
|
||||||
cb(parseFloat(time))
|
|
||||||
).catch((error) ->
|
|
||||||
console.error('vimeo::getCurrentTime():', error)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
cb(0)
|
|
||||||
|
|
||||||
getVolume: (cb) ->
|
|
||||||
if @vimeo
|
|
||||||
@vimeo.getVolume().then((volume) ->
|
|
||||||
cb(parseFloat(volume))
|
|
||||||
).catch((error) ->
|
|
||||||
console.error('vimeo::getVolume():', error)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
cb(VOLUME)
|
|
||||||
|
|
||||||
getRes: (cb) ->
|
|
||||||
if @vimeo
|
|
||||||
cb([@resx,@resy])
|
|
||||||
else
|
|
||||||
cb()
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
window.YouTubePlayer = class YouTubePlayer extends Player
|
|
||||||
constructor: (data) ->
|
|
||||||
if not (this instanceof YouTubePlayer)
|
|
||||||
return new YouTubePlayer(data)
|
|
||||||
|
|
||||||
@setMediaProperties(data)
|
|
||||||
@pauseSeekRaceCondition = false
|
|
||||||
|
|
||||||
waitUntilDefined(window, 'YT', =>
|
|
||||||
# Even after window.YT is defined, YT.Player may not be, which causes a
|
|
||||||
# 'YT.Player is not a constructor' error occasionally
|
|
||||||
waitUntilDefined(YT, 'Player', =>
|
|
||||||
removeOld()
|
|
||||||
|
|
||||||
wmode = if USEROPTS.wmode_transparent then 'transparent' else 'opaque'
|
|
||||||
@yt = new YT.Player('ytapiplayer',
|
|
||||||
videoId: data.id
|
|
||||||
playerVars:
|
|
||||||
autohide: 1
|
|
||||||
autoplay: 1
|
|
||||||
controls: 1
|
|
||||||
iv_load_policy: 3 # iv_load_policy 3 indicates no annotations
|
|
||||||
rel: 0
|
|
||||||
wmode: wmode
|
|
||||||
events:
|
|
||||||
onReady: @onReady.bind(this)
|
|
||||||
onStateChange: @onStateChange.bind(this)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
load: (data) ->
|
|
||||||
@setMediaProperties(data)
|
|
||||||
if @yt and @yt.ready
|
|
||||||
@yt.loadVideoById(data.id, data.currentTime)
|
|
||||||
else
|
|
||||||
console.error('WTF? YouTubePlayer::load() called but yt is not ready')
|
|
||||||
|
|
||||||
onReady: ->
|
|
||||||
@yt.ready = true
|
|
||||||
@setVolume(VOLUME)
|
|
||||||
|
|
||||||
onStateChange: (ev) ->
|
|
||||||
# If you pause the video before the first PLAYING
|
|
||||||
# event is emitted, weird things happen (or at least that was true
|
|
||||||
# whenever this comment was authored in 2015).
|
|
||||||
if ev.data == YT.PlayerState.PLAYING and @pauseSeekRaceCondition
|
|
||||||
@pause()
|
|
||||||
@pauseSeekRaceCondition = false
|
|
||||||
|
|
||||||
if (ev.data == YT.PlayerState.PAUSED and not @paused) or
|
|
||||||
(ev.data == YT.PlayerState.PLAYING and @paused)
|
|
||||||
@paused = (ev.data == YT.PlayerState.PAUSED)
|
|
||||||
if CLIENT.leader
|
|
||||||
sendVideoUpdate()
|
|
||||||
|
|
||||||
if ev.data == YT.PlayerState.ENDED and CLIENT.leader
|
|
||||||
socket.emit('playNext')
|
|
||||||
|
|
||||||
play: ->
|
|
||||||
@paused = false
|
|
||||||
if @yt and @yt.ready
|
|
||||||
@yt.playVideo()
|
|
||||||
|
|
||||||
pause: ->
|
|
||||||
@paused = true
|
|
||||||
if @yt and @yt.ready
|
|
||||||
@yt.pauseVideo()
|
|
||||||
|
|
||||||
seekTo: (time) ->
|
|
||||||
if @yt and @yt.ready
|
|
||||||
@yt.seekTo(time, true)
|
|
||||||
|
|
||||||
setVolume: (volume) ->
|
|
||||||
if @yt and @yt.ready
|
|
||||||
if volume > 0
|
|
||||||
# If the player is muted, even if the volume is set,
|
|
||||||
# the player remains muted
|
|
||||||
@yt.unMute()
|
|
||||||
@yt.setVolume(volume * 100)
|
|
||||||
|
|
||||||
setQuality: (quality) ->
|
|
||||||
# https://github.com/calzoneman/sync/issues/726
|
|
||||||
|
|
||||||
getTime: (cb) ->
|
|
||||||
if @yt and @yt.ready
|
|
||||||
cb(@yt.getCurrentTime())
|
|
||||||
else
|
|
||||||
cb(0)
|
|
||||||
|
|
||||||
getVolume: (cb) ->
|
|
||||||
if @yt and @yt.ready
|
|
||||||
if @yt.isMuted()
|
|
||||||
cb(0)
|
|
||||||
else
|
|
||||||
cb(@yt.getVolume() / 100)
|
|
||||||
else
|
|
||||||
cb(VOLUME)
|
|
||||||
|
|
@ -9,6 +9,4 @@ fi
|
||||||
|
|
||||||
echo "Building from src/ to lib/"
|
echo "Building from src/ to lib/"
|
||||||
npm run build-server
|
npm run build-server
|
||||||
echo "Building from player/ to www/js/player.js"
|
|
||||||
npm run build-player
|
|
||||||
echo "Done"
|
echo "Done"
|
||||||
|
|
|
||||||
|
|
@ -317,9 +317,9 @@ html(lang="en")
|
||||||
script(defer, src="https://player.vimeo.com/api/player.js")
|
script(defer, src="https://player.vimeo.com/api/player.js")
|
||||||
script(defer, src="/js/sc.js")
|
script(defer, src="/js/sc.js")
|
||||||
script(defer, src="/js/video.js")
|
script(defer, src="/js/video.js")
|
||||||
|
script(defer, src="/js/playerjs-0.0.12.js")
|
||||||
script(defer, src="/js/videojs-contrib-hls.min.js")
|
script(defer, src="/js/videojs-contrib-hls.min.js")
|
||||||
script(defer, src="/js/videojs-resolution-switcher.js")
|
script(defer, src="/js/videojs-resolution-switcher.js")
|
||||||
script(defer, src="/js/playerjs-0.0.12.js")
|
|
||||||
script(defer, src="/js/dash.all.min.js")
|
script(defer, src="/js/dash.all.min.js")
|
||||||
script(defer, src="/js/videojs-dash.js")
|
script(defer, src="/js/videojs-dash.js")
|
||||||
script(defer, src="https://player.twitch.tv/js/embed/v1.js")
|
script(defer, src="https://player.twitch.tv/js/embed/v1.js")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue