-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathVideoEncoder.js
More file actions
157 lines (135 loc) · 4.18 KB
/
VideoEncoder.js
File metadata and controls
157 lines (135 loc) · 4.18 KB
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
// Video Encode
// Like VideoDecoder.js, this marrys H264 encoding and Mp4 encoding
import PromiseQueue from './PromiseQueue.js'
import {ChunkArray} from './PopApi.js'
import {Mp4FragmentedEncoder} from './Mp4.js'
import {GetNaluMeta,SplitNalus} from './H264.js'
export class VideoEncoder_t
{
constructor()
{
this.InputFrameQueue = new PromiseQueue();
this.EncodeThreadPromise = this.EncodeThread();
}
PushFrame(Image,TimeMs,Keyframe=false)
{
if ( !Image )
return this.PushEndOfFile();
const Frame = {};
Frame.Image = Image;
Frame.TimeMs = TimeMs;
Frame.Keyframe = Keyframe;
this.InputFrameQueue.Push(Frame);
}
PushEndOfFile()
{
this.InputFrameQueue.Push(null);
}
OnError(Error)
{
// make encoder thread promise error
console.error(`Encoding error ${Error}`);
this.EncodeThreadPromise.Reject(Error);
}
async CreateH264Encoder(Image,OnFrameEncoded)
{
// gr: could do this on first frame output
const EncoderParams = {};
EncoderParams.output = OnFrameEncoded;
EncoderParams.error = this.OnError.bind(this);
const OutputVideoEncoder = new VideoEncoder(EncoderParams);
const EncoderConfig = {};
EncoderConfig.codec = 'avc1.42E01E'; // todo: construct properly
EncoderConfig.width = Image.width;
EncoderConfig.height = Image.height;
EncoderConfig.bitrate = 8 * 1024 * 1024 * 30;//32_000_000; // bits per second
EncoderConfig.bitrateMode = 'variable'; // 'constant'
//EncoderConfig.latencyMode = 'realtime';
EncoderConfig.latencyMode = 'quality';
//EncoderConfig.framerate = 30;
EncoderConfig.optimizeForLatency = true;
await OutputVideoEncoder.configure(EncoderConfig);
return OutputVideoEncoder;
}
async EncodeThread()
{
const Mp4Encoder = new Mp4FragmentedEncoder();
function OnFrameEncoded(EncodedVideoChunk,StreamMetaData)
{
const TrackId = 1;
if ( StreamMetaData && StreamMetaData.decoderConfig )
{
const Meta = StreamMetaData.decoderConfig;
const Codec = Meta.codec;
const ColourSpace = Meta.colorSpace;
const ExtraDataSpsPps = Meta.description;
console.log(`StreamMetaData`,StreamMetaData);
Mp4Encoder.PushExtraData( ExtraDataSpsPps, TrackId );
}
const Bytes = new Uint8Array( EncodedVideoChunk.byteLength );
const TimeMs = EncodedVideoChunk.timestamp / 1000;
EncodedVideoChunk.copyTo( Bytes );
console.log(`Encoded frame x${Bytes.length} @${TimeMs}`,EncodedVideoChunk);
Mp4Encoder.PushSample( Bytes, TimeMs, TimeMs, TrackId );
try
{
const Packets = SplitNalus(Bytes);
const Metas = Packets.map( GetNaluMeta );
Metas.forEach( m => console.log(`Encoded packet:`,m) );
}
catch(e)
{
console.warn(`Nalu error: ${e}`);
}
}
// wait for first frame
const FirstFrame = await this.InputFrameQueue.WaitForNext();
// create encoder
const H264Encoder = await this.CreateH264Encoder( FirstFrame.Image, OnFrameEncoded.bind(this) );
async function EncodeFrame(Frame)
{
const TimeMicro = Frame.TimeMs * 1000;
const FrameMeta = {};
//FrameMeta.duration = // microsecs
FrameMeta.timestamp = TimeMicro;// microsecs
// for array buffer
//FrameMeta.format = "RGBX";
const Bitmap = await createImageBitmap(Frame.Image);
const EncodeFrame = new VideoFrame( Bitmap, FrameMeta );
const EncodeOptions = {};
//EncodeOptions.keyFrame = true;
H264Encoder.encode( EncodeFrame, EncodeOptions );
// discard VideoFrame we just created (get a warning from browser otherwise)
EncodeFrame.close();
}
// encode first frame
await EncodeFrame( FirstFrame );
// wait for more frames
// wait for EOF
while ( true )
{
const NextFrame = await this.InputFrameQueue.WaitForNext();
if ( !NextFrame )
break;
await EncodeFrame( NextFrame );
}
// wait for encoder to finish before marking mp4 finished
await H264Encoder.flush();
Mp4Encoder.PushEndOfFile();
// wait for mp4 encoder to finish
const Mp4Datas = new ChunkArray();
while ( true )
{
const Mp4Chunk = await Mp4Encoder.WaitForNextEncodedBytes();
if ( !Mp4Chunk )
break;
Mp4Datas.push( Mp4Chunk );
}
const FinalMp4Data = Mp4Datas.slice();
return FinalMp4Data;
}
async WaitForEncodedData()
{
return this.EncodeThreadPromise;
}
}