Merge branch '3.0' of https://github.com/calzoneman/sync into 3.0
[KisSync.git] / player / videojs.coffee
blob32c7e668b3c3fbe1c13cb94585745c5416c9e9f8
1 sortSources = (sources) ->
2     if not sources
3         console.error('sortSources() called with null source list')
4         return []
6     qualities = ['2160', '1440', '1080', '720', '540', '480', '360', '240']
7     pref = String(USEROPTS.default_quality)
8     if USEROPTS.default_quality == 'best'
9         pref = '2160'
10     idx = qualities.indexOf(pref)
11     if idx < 0
12         idx = 5 # 480p
14     qualityOrder = qualities.slice(idx).concat(qualities.slice(0, idx).reverse())
15     qualityOrder.unshift('auto')
16     sourceOrder = []
17     flvOrder = []
18     for quality in qualityOrder
19         if quality of sources
20             flv = []
21             nonflv = []
22             sources[quality].forEach((source) ->
23                 source.quality = quality
24                 if source.contentType == 'video/flv'
25                     flv.push(source)
26                 else
27                     nonflv.push(source)
28             )
29             sourceOrder = sourceOrder.concat(nonflv)
30             flvOrder = flvOrder.concat(flv)
32     return sourceOrder.concat(flvOrder).map((source) ->
33         type: source.contentType
34         src: source.link
35         res: source.quality
36         label: getSourceLabel(source)
37     )
39 getSourceLabel = (source) ->
40     if source.res is 'auto'
41         return 'auto'
42     else
43         return "#{source.quality}p #{source.contentType.split('/')[1]}"
45 waitUntilDefined(window, 'videojs', =>
46     videojs.options.flash.swf = '/video-js.swf'
49 hasAnyTextTracks = (data) ->
50     ntracks = data?.meta?.textTracks?.length ? 0
51     return ntracks > 0
53 window.VideoJSPlayer = class VideoJSPlayer extends Player
54     constructor: (data) ->
55         if not (this instanceof VideoJSPlayer)
56             return new VideoJSPlayer(data)
58         @load(data)
60     loadPlayer: (data) ->
61         waitUntilDefined(window, 'videojs', =>
62             attrs =
63                 width: '100%'
64                 height: '100%'
66             if @mediaType == 'cm' and hasAnyTextTracks(data)
67                 attrs.crossorigin = 'anonymous'
69             video = $('<video/>')
70                 .addClass('video-js vjs-default-skin embed-responsive-item')
71                 .attr(attrs)
72             removeOld(video)
74             @sources = sortSources(data.meta.direct)
75             if @sources.length == 0
76                 console.error('VideoJSPlayer::constructor(): data.meta.direct
77                                has no sources!')
78                 @mediaType = null
79                 return
81             @sourceIdx = 0
83             # TODO: Refactor VideoJSPlayer to use a preLoad()/load()/postLoad() pattern
84             # VideoJSPlayer should provide the core functionality and logic for specific
85             # dependent player types (gdrive) should be an extension
86             if data.meta.gdrive_subtitles
87                 data.meta.gdrive_subtitles.available.forEach((subt) ->
88                     label = subt.lang_original
89                     if subt.name
90                         label += " (#{subt.name})"
91                     $('<track/>').attr(
92                         src: "/gdvtt/#{data.id}/#{subt.lang}/#{subt.name}.vtt?\
93                                 vid=#{data.meta.gdrive_subtitles.vid}"
94                         kind: 'subtitles'
95                         srclang: subt.lang
96                         label: label
97                     ).appendTo(video)
98                 )
100             if data.meta.textTracks
101                 data.meta.textTracks.forEach((track) ->
102                     label = track.name
103                     attrs =
104                         src: track.url
105                         kind: 'subtitles'
106                         type: track.type
107                         label: label
109                     if track.default? and track.default
110                         attrs.default = ''
112                     $('<track/>').attr(attrs).appendTo(video)
113                 )
115             @player = videojs(video[0],
116                     # https://github.com/Dash-Industry-Forum/dash.js/issues/2184
117                     autoplay: @sources[0].type != 'application/dash+xml',
118                     controls: true,
119                     plugins:
120                         videoJsResolutionSwitcher:
121                             default: @sources[0].res
122             )
123             @player.ready(=>
124                 # Have to use updateSrc instead of <source> tags
125                 # see: https://github.com/videojs/video.js/issues/3428
126                 @player.updateSrc(@sources)
127                 @player.on('error', =>
128                     err = @player.error()
129                     if err and err.code == 4
130                         console.error('Caught error, trying next source')
131                         # Does this really need to be done manually?
132                         @sourceIdx++
133                         if @sourceIdx < @sources.length
134                             @player.src(@sources[@sourceIdx])
135                         else
136                             console.error('Out of sources, video will not play')
137                             if @mediaType is 'gd'
138                                 if not window.hasDriveUserscript
139                                     window.promptToInstallDriveUserscript()
140                                 else
141                                     window.tellUserNotToContactMeAboutThingsThatAreNotSupported()
142                 )
143                 @setVolume(VOLUME)
144                 @player.on('ended', ->
145                     if CLIENT.leader
146                         socket.emit('playNext')
147                 )
149                 @player.on('pause', =>
150                     @paused = true
151                     if CLIENT.leader
152                         sendVideoUpdate()
153                 )
155                 @player.on('play', =>
156                     @paused = false
157                     if CLIENT.leader
158                         sendVideoUpdate()
159                 )
161                 # Workaround for IE-- even after seeking completes, the loading
162                 # spinner remains.
163                 @player.on('seeked', =>
164                     $('.vjs-waiting').removeClass('vjs-waiting')
165                 )
167                 # Workaround for Chrome-- it seems that the click bindings for
168                 # the subtitle menu aren't quite set up until after the ready
169                 # event finishes, so set a timeout for 1ms to force this code
170                 # not to run until the ready() function returns.
171                 setTimeout(->
172                     $('#ytapiplayer .vjs-subtitles-button .vjs-menu-item').each((i, elem) ->
173                         textNode = elem.childNodes[0]
174                         if textNode.textContent == localStorage.lastSubtitle
175                             elem.click()
177                         elem.onclick = ->
178                             if elem.attributes['aria-checked'].value == 'true'
179                                 localStorage.lastSubtitle = textNode.textContent
180                     )
181                 , 1)
182             )
183         )
185     load: (data) ->
186         @setMediaProperties(data)
187         # Note: VideoJS does have facilities for loading new videos into the
188         # existing player object, however it appears to be pretty glitchy when
189         # a video can't be played (either previous or next video).  It's safer
190         # to just reset the entire thing.
191         @destroy()
192         @loadPlayer(data)
194     play: ->
195         @paused = false
196         if @player and @player.readyState() > 0
197             @player.play()
199     pause: ->
200         @paused = true
201         if @player and @player.readyState() > 0
202             @player.pause()
204     seekTo: (time) ->
205         if @player and @player.readyState() > 0
206             @player.currentTime(time)
208     setVolume: (volume) ->
209         if @player
210             @player.volume(volume)
212     getTime: (cb) ->
213         if @player and @player.readyState() > 0
214             cb(@player.currentTime())
215         else
216             cb(0)
218     getVolume: (cb) ->
219         if @player and @player.readyState() > 0
220             if @player.muted()
221                 cb(0)
222             else
223                 cb(@player.volume())
224         else
225             cb(VOLUME)
227     destroy: ->
228         removeOld()
229         if @player
230             @player.dispose()