aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/assets/player')
-rw-r--r--client/src/assets/player/peertube-videojs-plugin.ts238
-rw-r--r--client/src/assets/player/video-renderer.ts119
2 files changed, 357 insertions, 0 deletions
diff --git a/client/src/assets/player/peertube-videojs-plugin.ts b/client/src/assets/player/peertube-videojs-plugin.ts
new file mode 100644
index 000000000..090cc53ba
--- /dev/null
+++ b/client/src/assets/player/peertube-videojs-plugin.ts
@@ -0,0 +1,238 @@
1// Big thanks to: https://github.com/kmoskwiak/videojs-resolution-switcher
2
3import videojs, { Player } from 'video.js'
4import * as WebTorrent from 'webtorrent'
5
6import { renderVideo } from './video-renderer'
7import { VideoFile } from '../../../../shared'
8
9// videojs typings don't have some method we need
10const videojsUntyped = videojs as any
11const webtorrent = new WebTorrent({ dht: false })
12
13const MenuItem = videojsUntyped.getComponent('MenuItem')
14const ResolutionMenuItem = videojsUntyped.extend(MenuItem, {
15 constructor: function (player: Player, options) {
16 options.selectable = true
17 MenuItem.call(this, player, options)
18
19 const currentResolution = this.player_.getCurrentResolution()
20 this.selected(this.options_.id === currentResolution)
21 },
22
23 handleClick: function (event) {
24 MenuItem.prototype.handleClick.call(this, event)
25 this.player_.updateResolution(this.options_.id)
26 }
27})
28MenuItem.registerComponent('ResolutionMenuItem', ResolutionMenuItem)
29
30const MenuButton = videojsUntyped.getComponent('MenuButton')
31const ResolutionMenuButton = videojsUntyped.extend(MenuButton, {
32 constructor: function (player, options) {
33 this.label = document.createElement('span')
34 options.label = 'Quality'
35
36 MenuButton.call(this, player, options)
37 this.el().setAttribute('aria-label', 'Quality')
38 this.controlText('Quality')
39
40 videojsUntyped.dom.addClass(this.label, 'vjs-resolution-button-label')
41 this.el().appendChild(this.label)
42
43 player.on('videoFileUpdate', videojs.bind(this, this.update))
44 },
45
46 createItems: function () {
47 const menuItems = []
48 for (const videoFile of this.player_.videoFiles) {
49 menuItems.push(new ResolutionMenuItem(
50 this.player_,
51 {
52 id: videoFile.resolution,
53 label: videoFile.resolutionLabel,
54 src: videoFile.magnetUri,
55 selected: videoFile.resolution === this.currentSelection
56 })
57 )
58 }
59
60 return menuItems
61 },
62
63 update: function () {
64 this.label.innerHTML = this.player_.getCurrentResolutionLabel()
65 return MenuButton.prototype.update.call(this)
66 },
67
68 buildCSSClass: function () {
69 return MenuButton.prototype.buildCSSClass.call(this) + ' vjs-resolution-button'
70 }
71})
72MenuButton.registerComponent('ResolutionMenuButton', ResolutionMenuButton)
73
74const Button = videojsUntyped.getComponent('Button')
75const PeertubeLinkButton = videojsUntyped.extend(Button, {
76 constructor: function (player) {
77 Button.apply(this, arguments)
78 this.player = player
79 },
80
81 createEl: function () {
82 const link = document.createElement('a')
83 link.href = window.location.href.replace('embed', 'watch')
84 link.innerHTML = 'PeerTube'
85 link.title = 'Go to the video page'
86 link.className = 'vjs-peertube-link'
87 link.target = '_blank'
88
89 return link
90 },
91
92 handleClick: function () {
93 this.player.pause()
94 }
95})
96Button.registerComponent('PeerTubeLinkButton', PeertubeLinkButton)
97
98type PeertubePluginOptions = {
99 videoFiles: VideoFile[]
100 playerElement: HTMLVideoElement
101 autoplay: boolean
102 peerTubeLink: boolean
103}
104const peertubePlugin = function (options: PeertubePluginOptions) {
105 const player = this
106 let currentVideoFile: VideoFile = undefined
107 const playerElement = options.playerElement
108 player.videoFiles = options.videoFiles
109
110 // Hack to "simulate" src link in video.js >= 6
111 // Without this, we can't play the video after pausing it
112 // https://github.com/videojs/video.js/blob/master/src/js/player.js#L1633
113 player.src = function () {
114 return true
115 }
116
117 player.getCurrentResolution = function () {
118 return currentVideoFile ? currentVideoFile.resolution : -1
119 }
120
121 player.getCurrentResolutionLabel = function () {
122 return currentVideoFile ? currentVideoFile.resolutionLabel : ''
123 }
124
125 player.updateVideoFile = function (videoFile: VideoFile, done: () => void) {
126 if (done === undefined) {
127 done = () => { /* empty */ }
128 }
129
130 // Pick the first one
131 if (videoFile === undefined) {
132 videoFile = player.videoFiles[0]
133 }
134
135 // Don't add the same video file once again
136 if (currentVideoFile !== undefined && currentVideoFile.magnetUri === videoFile.magnetUri) {
137 return
138 }
139
140 const previousVideoFile = currentVideoFile
141 currentVideoFile = videoFile
142
143 console.log('Adding ' + videoFile.magnetUri + '.')
144 player.torrent = webtorrent.add(videoFile.magnetUri, torrent => {
145 console.log('Added ' + videoFile.magnetUri + '.')
146
147 this.flushVideoFile(previousVideoFile)
148
149 const options = { autoplay: true, controls: true }
150 renderVideo(torrent.files[0], playerElement, options,(err, renderer) => {
151 if (err) return handleError(err)
152
153 this.renderer = renderer
154 player.play()
155
156 return done()
157 })
158 })
159
160 player.torrent.on('error', err => handleError(err))
161 player.torrent.on('warning', err => handleError(err))
162
163 player.trigger('videoFileUpdate')
164
165 return player
166 }
167
168 player.updateResolution = function (resolution) {
169 // Remember player state
170 const currentTime = player.currentTime()
171 const isPaused = player.paused()
172
173 // Hide bigPlayButton
174 if (!isPaused && this.player_.options_.bigPlayButton) {
175 this.player_.bigPlayButton.hide()
176 }
177
178 const newVideoFile = player.videoFiles.find(f => f.resolution === resolution)
179 player.updateVideoFile(newVideoFile, () => {
180 player.currentTime(currentTime)
181 player.handleTechSeeked_()
182 })
183 }
184
185 player.flushVideoFile = function (videoFile: VideoFile, destroyRenderer = true) {
186 if (videoFile !== undefined && webtorrent.get(videoFile.magnetUri)) {
187 if (destroyRenderer === true) this.renderer.destroy()
188 webtorrent.remove(videoFile.magnetUri)
189 }
190 }
191
192 player.ready(function () {
193 const controlBar = player.controlBar
194
195 const menuButton = new ResolutionMenuButton(player, options)
196 const fullscreenElement = controlBar.fullscreenToggle.el()
197 controlBar.resolutionSwitcher = controlBar.el().insertBefore(menuButton.el(), fullscreenElement)
198 controlBar.resolutionSwitcher.dispose = function () {
199 this.parentNode.removeChild(this)
200 }
201
202 player.dispose = function () {
203 // Don't need to destroy renderer, video player will be destroyed
204 player.flushVideoFile(currentVideoFile, false)
205 }
206
207 if (options.peerTubeLink === true) {
208 const peerTubeLinkButton = new PeertubeLinkButton(player)
209 controlBar.peerTubeLink = controlBar.el().insertBefore(peerTubeLinkButton.el(), fullscreenElement)
210
211 controlBar.peerTubeLink.dispose = function () {
212 this.parentNode.removeChild(this)
213 }
214 }
215
216 if (options.autoplay === true) {
217 player.updateVideoFile()
218 } else {
219 player.one('play', () => player.updateVideoFile())
220 }
221
222 setInterval(() => {
223 if (player.torrent !== undefined) {
224 player.trigger('torrentInfo', {
225 downloadSpeed: player.torrent.downloadSpeed,
226 numPeers: player.torrent.numPeers,
227 uploadSpeed: player.torrent.uploadSpeed
228 })
229 }
230 }, 1000)
231 })
232
233 function handleError (err: Error|string) {
234 return player.trigger('customError', { err })
235 }
236}
237
238videojsUntyped.registerPlugin('peertube', peertubePlugin)
diff --git a/client/src/assets/player/video-renderer.ts b/client/src/assets/player/video-renderer.ts
new file mode 100644
index 000000000..8baa42533
--- /dev/null
+++ b/client/src/assets/player/video-renderer.ts
@@ -0,0 +1,119 @@
1// Thanks: https://github.com/feross/render-media
2// TODO: use render-media once https://github.com/feross/render-media/issues/32 is fixed
3
4import { extname } from 'path'
5import * as MediaElementWrapper from 'mediasource'
6import * as videostream from 'videostream'
7
8const VIDEOSTREAM_EXTS = [
9 '.m4a',
10 '.m4v',
11 '.mp4'
12]
13
14type RenderMediaOptions = {
15 controls: boolean
16 autoplay: boolean
17}
18
19function renderVideo (
20 file,
21 elem: HTMLVideoElement,
22 opts: RenderMediaOptions,
23 callback: (err: Error, renderer: any) => void
24) {
25 validateFile(file)
26
27 return renderMedia(file, elem, opts, callback)
28}
29
30function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer: any) => void) {
31 const extension = extname(file.name).toLowerCase()
32 let preparedElem = undefined
33 let currentTime = 0
34 let renderer
35
36 if (VIDEOSTREAM_EXTS.indexOf(extension) >= 0) {
37 renderer = useVideostream()
38 } else {
39 renderer = useMediaSource()
40 }
41
42 function useVideostream () {
43 prepareElem()
44 preparedElem.addEventListener('error', fallbackToMediaSource)
45 preparedElem.addEventListener('loadstart', onLoadStart)
46 preparedElem.addEventListener('canplay', onCanPlay)
47 return videostream(file, preparedElem)
48 }
49
50 function useMediaSource () {
51 prepareElem()
52 preparedElem.addEventListener('error', callback)
53 preparedElem.addEventListener('loadstart', onLoadStart)
54 preparedElem.addEventListener('canplay', onCanPlay)
55
56 const wrapper = new MediaElementWrapper(preparedElem)
57 const writable = wrapper.createWriteStream(getCodec(file.name))
58 file.createReadStream().pipe(writable)
59
60 if (currentTime) preparedElem.currentTime = currentTime
61
62 return wrapper
63 }
64
65 function fallbackToMediaSource () {
66 preparedElem.removeEventListener('error', fallbackToMediaSource)
67 preparedElem.removeEventListener('canplay', onCanPlay)
68
69 useMediaSource()
70 }
71
72 function prepareElem () {
73 if (preparedElem === undefined) {
74 preparedElem = elem
75
76 preparedElem.addEventListener('progress', function () {
77 currentTime = elem.currentTime
78 })
79 }
80 }
81
82 function onLoadStart () {
83 preparedElem.removeEventListener('loadstart', onLoadStart)
84 if (opts.autoplay) preparedElem.play()
85 }
86
87 function onCanPlay () {
88 preparedElem.removeEventListener('canplay', onCanPlay)
89 callback(null, renderer)
90 }
91}
92
93function validateFile (file) {
94 if (file == null) {
95 throw new Error('file cannot be null or undefined')
96 }
97 if (typeof file.name !== 'string') {
98 throw new Error('missing or invalid file.name property')
99 }
100 if (typeof file.createReadStream !== 'function') {
101 throw new Error('missing or invalid file.createReadStream property')
102 }
103}
104
105function getCodec (name: string) {
106 const ext = extname(name).toLowerCase()
107 return {
108 '.m4a': 'audio/mp4; codecs="mp4a.40.5"',
109 '.m4v': 'video/mp4; codecs="avc1.640029, mp4a.40.5"',
110 '.mkv': 'video/webm; codecs="avc1.640029, mp4a.40.5"',
111 '.mp3': 'audio/mpeg',
112 '.mp4': 'video/mp4; codecs="avc1.640029, mp4a.40.5"',
113 '.webm': 'video/webm; codecs="vorbis, vp8"'
114 }[ext]
115}
116
117export {
118 renderVideo
119}