-
Notifications
You must be signed in to change notification settings - Fork 19
/
video-process-worker.js
171 lines (168 loc) · 5.53 KB
/
video-process-worker.js
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// Modified based on https://github.com/w3c/webcodecs/tree/main/samples/mp4-decode
import MP4Box from 'mp4box'
const getConfig = (info, mp4File) => {
const track = info.videoTracks[0]
// get description, adapted from
// https://github.com/w3c/webcodecs/blob/main/samples/video-decode-display/demuxer_mp4.js#L64
let description = undefined
for (const entry of mp4File.moov.traks[0].mdia.minf.stbl.stsd.entries) {
if (entry.avcC || entry.hvcC) {
const stream = new MP4Box.DataStream(undefined, 0, MP4Box.DataStream.BIG_ENDIAN)
if (entry.avcC) {
entry.avcC.write(stream)
} else {
entry.hvcC.write(stream)
}
description = new Uint8Array(stream.buffer, 8) // Remove the box header.
}
}
return {
codec: track.codec,
codedHeight: track.track_height,
codedWidth: track.track_width,
description: description
}
}
const probeUnit = 512 * 1024
onmessage = async (event) => {
try {
// demux and decode the video
const mp4File = MP4Box.createFile()
let decoder
const offscreen = new OffscreenCanvas(0, 0)
const ctx = offscreen.getContext('2d')
let isMoovFound = false
mp4File.onReady = (info) => {
isMoovFound = true
const videoTrack = info.tracks.find((track) => track.type === 'video')
const { id, track_width, track_height, nb_samples, movie_duration, movie_timescale } = videoTrack
offscreen.width = track_width
offscreen.height = track_height
const duration = movie_duration / movie_timescale
const probeFps = nb_samples / duration
const probeFrames = nb_samples
const fps = probeFps < event.data.defaultFps ? probeFps : event.data.defaultFps
const frames = Math.floor(fps * duration)
postMessage({
videoTrackInfo: {
width: track_width,
height: track_height,
duration: duration,
frames,
fps
}
})
const frameIndexList = []
for (let i = 0; i < frames; i++) {
frameIndexList.push(Math.floor((i * probeFrames) / frames))
}
let currentFrameIndex = 0
decoder = new VideoDecoder({
output: (frame) => {
const _currentFrameIndex = currentFrameIndex
if (frameIndexList.includes(_currentFrameIndex)) {
ctx.drawImage(frame, 0, 0)
offscreen.convertToBlob({ type: 'image/jpeg' }).then((blob) => {
postMessage({
frame: blob,
frameIndex: frameIndexList.indexOf(_currentFrameIndex)
})
})
if (frameIndexList.indexOf(_currentFrameIndex) >= frameIndexList.length - 3) {
// TODO: hack, safe margin
postMessage({ done: true })
}
}
frame.close()
currentFrameIndex += 1
},
error: (error) => console.error('VideoDecoder: ', error)
})
decoder.configure(getConfig(info, mp4File))
mp4File.setExtractionOptions(id)
mp4File.start()
}
mp4File.onError = (error) => {
console.error('MP4Box: ', error)
}
mp4File.onSamples = (id, user, samples) => {
for (let sample of samples) {
const type = sample.is_sync ? 'key' : 'delta'
const chunk = new EncodedVideoChunk({
type: type,
timestamp: sample.cts,
duration: sample.duration,
data: sample.data
})
decoder.decode(chunk)
}
}
let leftOffset = 0
let rightOffset = 0
let contentLength = 0
const fetchVideo = async (isLeft) => {
try {
const abortController = new AbortController()
const headers = {}
if (isLeft) {
headers['Range'] = `bytes=${leftOffset}-`
} else {
headers['Range'] = `bytes=${rightOffset}-${rightOffset + probeUnit - 1}`
}
const response = await fetch(event.data.src, {
signal: abortController.signal,
headers
})
if (!response.ok) {
throw {
type: 'fetch',
status: response.status,
statusText: response.statusText
}
}
if (isLeft && contentLength === 0) {
contentLength = response.headers.get('Content-Length')
rightOffset = contentLength - probeUnit
}
const reader = response.body.getReader()
let offset = isLeft ? leftOffset : rightOffset
const initOffset = offset
while (true) {
const { done, value } = await reader.read()
if (done) {
if (isLeft && isMoovFound) {
mp4File.flush()
return true
} else if (isLeft && !isMoovFound) {
console.error('Error')
return false
} else return !isLeft && isMoovFound
}
const buffer = value.buffer
buffer.fileStart = isLeft ? leftOffset : offset
offset += buffer.byteLength
if (isLeft) leftOffset += buffer.byteLength
mp4File.appendBuffer(buffer)
if (!isMoovFound && buffer.fileStart - initOffset > probeUnit) {
abortController.abort()
}
}
} catch (e) {
if (e.toString().includes('AbortError')) return false
else throw e
}
}
let found = false
let isLeft = true
while (!found && rightOffset - leftOffset > -probeUnit) {
found = await fetchVideo(isLeft)
if (!found && !isLeft) rightOffset -= probeUnit
isLeft = !isLeft
}
if (isLeft) {
await fetchVideo(isLeft)
}
} catch (e) {
postMessage({ error: e })
}
}