Make it works on iOS
[oweals/peertube.git] / client / src / assets / player / video-renderer.ts
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
4 import * as MediaElementWrapper from 'mediasource'
5 import { extname } from 'path'
6 import * as videostream from 'videostream'
7
8 const VIDEOSTREAM_EXTS = [
9   '.m4a',
10   '.m4v',
11   '.mp4'
12 ]
13
14 type RenderMediaOptions = {
15   controls: boolean
16   autoplay: boolean
17 }
18
19 function 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
30 function 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   try {
37     if (VIDEOSTREAM_EXTS.indexOf(extension) >= 0) {
38       renderer = useVideostream()
39     } else {
40       renderer = useMediaSource()
41     }
42   } catch (err) {
43     return callback(err)
44   }
45
46   function useVideostream () {
47     prepareElem()
48     preparedElem.addEventListener('error', function onError () {
49       preparedElem.removeEventListener('error', onError)
50
51       return fallbackToMediaSource()
52     })
53     preparedElem.addEventListener('loadstart', onLoadStart)
54     return videostream(file, preparedElem)
55   }
56
57   function useMediaSource (useVP9 = false) {
58     const codecs = getCodec(file.name, useVP9)
59
60     prepareElem()
61     preparedElem.addEventListener('error', function onError(err) {
62       // Try with vp9 before returning an error
63       if (codecs.indexOf('vp8') !== -1) {
64         preparedElem.removeEventListener('error', onError)
65
66         return fallbackToMediaSource(true)
67       }
68
69       return callback(err)
70     })
71     preparedElem.addEventListener('loadstart', onLoadStart)
72
73     const wrapper = new MediaElementWrapper(preparedElem)
74     const writable = wrapper.createWriteStream(codecs)
75     file.createReadStream().pipe(writable)
76
77     if (currentTime) preparedElem.currentTime = currentTime
78
79     return wrapper
80   }
81
82   function fallbackToMediaSource (useVP9 = false) {
83     if (useVP9 === true) console.log('Falling back to media source with VP9 enabled.')
84     else console.log('Falling back to media source..')
85
86     useMediaSource(useVP9)
87   }
88
89   function prepareElem () {
90     if (preparedElem === undefined) {
91       preparedElem = elem
92
93       preparedElem.addEventListener('progress', function () {
94         currentTime = elem.currentTime
95       })
96     }
97   }
98
99   function onLoadStart () {
100     preparedElem.removeEventListener('loadstart', onLoadStart)
101     if (opts.autoplay) preparedElem.play()
102
103     callback(null, renderer)
104   }
105 }
106
107 function validateFile (file) {
108   if (file == null) {
109     throw new Error('file cannot be null or undefined')
110   }
111   if (typeof file.name !== 'string') {
112     throw new Error('missing or invalid file.name property')
113   }
114   if (typeof file.createReadStream !== 'function') {
115     throw new Error('missing or invalid file.createReadStream property')
116   }
117 }
118
119 function getCodec (name: string, useVP9 = false) {
120   const ext = extname(name).toLowerCase()
121   if (ext === '.mp4') {
122     return 'video/mp4; codecs="avc1.640029, mp4a.40.5"'
123   }
124
125   if (ext === '.webm') {
126     if (useVP9 === true) return 'video/webm; codecs="vp9, opus"'
127
128     return 'video/webm; codecs="vp8, vorbis"'
129   }
130
131   return undefined
132 }
133
134 export {
135   renderVideo
136 }