aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player/video-renderer.ts
blob: e3415abd336c25e028ee098736fb76c2dfed8652 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// Thanks: https://github.com/feross/render-media
// TODO: use render-media once https://github.com/feross/render-media/issues/32 is fixed

import * as MediaElementWrapper from 'mediasource'
import { extname } from 'path'
import * as videostream from 'videostream'

const VIDEOSTREAM_EXTS = [
  '.m4a',
  '.m4v',
  '.mp4'
]

type RenderMediaOptions = {
  controls: boolean
  autoplay: boolean
}

function renderVideo (
  file,
  elem: HTMLVideoElement,
  opts: RenderMediaOptions,
  callback: (err: Error, renderer: any) => void
) {
  validateFile(file)

  return renderMedia(file, elem, opts, callback)
}

function renderMedia (file, elem: HTMLVideoElement, opts: RenderMediaOptions, callback: (err: Error, renderer?: any) => void) {
  const extension = extname(file.name).toLowerCase()
  let preparedElem = undefined
  let currentTime = 0
  let renderer

  try {
    if (VIDEOSTREAM_EXTS.indexOf(extension) >= 0) {
      renderer = useVideostream()
    } else {
      renderer = useMediaSource()
    }
  } catch (err) {
    return callback(err)
  }

  function useVideostream () {
    prepareElem()
    preparedElem.addEventListener('error', function onError () {
      preparedElem.removeEventListener('error', onError)

      return fallbackToMediaSource()
    })
    preparedElem.addEventListener('loadstart', onLoadStart)
    return videostream(file, preparedElem)
  }

  function useMediaSource (useVP9 = false) {
    const codecs = getCodec(file.name, useVP9)

    prepareElem()
    preparedElem.addEventListener('error', function onError(err) {
      // Try with vp9 before returning an error
      if (codecs.indexOf('vp8') !== -1) {
        preparedElem.removeEventListener('error', onError)

        return fallbackToMediaSource(true)
      }

      return callback(err)
    })
    preparedElem.addEventListener('loadstart', onLoadStart)

    const wrapper = new MediaElementWrapper(preparedElem)
    const writable = wrapper.createWriteStream(codecs)
    file.createReadStream().pipe(writable)

    if (currentTime) preparedElem.currentTime = currentTime

    return wrapper
  }

  function fallbackToMediaSource (useVP9 = false) {
    if (useVP9 === true) console.log('Falling back to media source with VP9 enabled.')
    else console.log('Falling back to media source..')

    useMediaSource(useVP9)
  }

  function prepareElem () {
    if (preparedElem === undefined) {
      preparedElem = elem

      preparedElem.addEventListener('progress', function () {
        currentTime = elem.currentTime
      })
    }
  }

  function onLoadStart () {
    preparedElem.removeEventListener('loadstart', onLoadStart)
    if (opts.autoplay) preparedElem.play()

    callback(null, renderer)
  }
}

function validateFile (file) {
  if (file == null) {
    throw new Error('file cannot be null or undefined')
  }
  if (typeof file.name !== 'string') {
    throw new Error('missing or invalid file.name property')
  }
  if (typeof file.createReadStream !== 'function') {
    throw new Error('missing or invalid file.createReadStream property')
  }
}

function getCodec (name: string, useVP9 = false) {
  const ext = extname(name).toLowerCase()
  if (ext === '.mp4') {
    return 'video/mp4; codecs="avc1.640029, mp4a.40.5"'
  }

  if (ext === '.webm') {
    if (useVP9 === true) return 'video/webm; codecs="vp9, opus"'

    return 'video/webm; codecs="vp8, vorbis"'
  }

  return undefined
}

export {
  renderVideo
}