aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/assets/player/video-renderer.ts
blob: 8baa425330c36399ac78a0a76e3fee8cb54ba888 (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
// Thanks: https://github.com/feross/render-media
// TODO: use render-media once https://github.com/feross/render-media/issues/32 is fixed

import { extname } from 'path'
import * as MediaElementWrapper from 'mediasource'
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

  if (VIDEOSTREAM_EXTS.indexOf(extension) >= 0) {
    renderer = useVideostream()
  } else {
    renderer = useMediaSource()
  }

  function useVideostream () {
    prepareElem()
    preparedElem.addEventListener('error', fallbackToMediaSource)
    preparedElem.addEventListener('loadstart', onLoadStart)
    preparedElem.addEventListener('canplay', onCanPlay)
    return videostream(file, preparedElem)
  }

  function useMediaSource () {
    prepareElem()
    preparedElem.addEventListener('error', callback)
    preparedElem.addEventListener('loadstart', onLoadStart)
    preparedElem.addEventListener('canplay', onCanPlay)

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

    if (currentTime) preparedElem.currentTime = currentTime

    return wrapper
  }

  function fallbackToMediaSource () {
    preparedElem.removeEventListener('error', fallbackToMediaSource)
    preparedElem.removeEventListener('canplay', onCanPlay)

    useMediaSource()
  }

  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()
  }

  function onCanPlay () {
    preparedElem.removeEventListener('canplay', onCanPlay)
    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) {
  const ext = extname(name).toLowerCase()
  return {
    '.m4a': 'audio/mp4; codecs="mp4a.40.5"',
    '.m4v': 'video/mp4; codecs="avc1.640029, mp4a.40.5"',
    '.mkv': 'video/webm; codecs="avc1.640029, mp4a.40.5"',
    '.mp3': 'audio/mpeg',
    '.mp4': 'video/mp4; codecs="avc1.640029, mp4a.40.5"',
    '.webm': 'video/webm; codecs="vorbis, vp8"'
  }[ext]
}

export {
  renderVideo
}