OggDemuxer.js
3.76 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
const { Transform } = require('stream');
const OGG_PAGE_HEADER_SIZE = 26;
const STREAM_STRUCTURE_VERSION = 0;
const charCode = x => x.charCodeAt(0);
const OGGS_HEADER = Buffer.from([...'OggS'].map(charCode));
const OPUS_HEAD = Buffer.from([...'OpusHead'].map(charCode));
const OPUS_TAGS = Buffer.from([...'OpusTags'].map(charCode));
/**
* Demuxes an Ogg stream (containing Opus audio) to output an Opus stream.
* @extends {TransformStream}
* @memberof opus
*/
class OggDemuxer extends Transform {
/**
* Creates a new OggOpus demuxer.
* @param {Object} [options] options that you would pass to a regular Transform stream.
* @memberof opus
*/
constructor(options = {}) {
super(Object.assign({ readableObjectMode: true }, options));
this._remainder = null;
this._head = null;
this._bitstream = null;
}
_transform(chunk, encoding, done) {
if (this._remainder) {
chunk = Buffer.concat([this._remainder, chunk]);
this._remainder = null;
}
try {
while (chunk) {
const result = this._readPage(chunk);
if (result) chunk = result;
else break;
}
} catch (error) {
done(error);
return;
}
this._remainder = chunk;
done();
}
/**
* Reads a page from a buffer
* @private
* @param {Buffer} chunk the chunk containing the page
* @returns {boolean|Buffer} if a buffer, it will be a slice of the excess data of the original, otherwise it will be
* false and would indicate that there is not enough data to go ahead with reading this page.
*/
_readPage(chunk) {
if (chunk.length < OGG_PAGE_HEADER_SIZE) {
return false;
}
if (!chunk.slice(0, 4).equals(OGGS_HEADER)) {
throw Error(`capture_pattern is not ${OGGS_HEADER}`);
}
if (chunk.readUInt8(4) !== STREAM_STRUCTURE_VERSION) {
throw Error(`stream_structure_version is not ${STREAM_STRUCTURE_VERSION}`);
}
if (chunk.length < 27) return false;
const pageSegments = chunk.readUInt8(26);
if (chunk.length < 27 + pageSegments) return false;
const table = chunk.slice(27, 27 + pageSegments);
const bitstream = chunk.readUInt32BE(14);
let sizes = [], totalSize = 0;
for (let i = 0; i < pageSegments;) {
let size = 0, x = 255;
while (x === 255) {
if (i >= table.length) return false;
x = table.readUInt8(i);
i++;
size += x;
}
sizes.push(size);
totalSize += size;
}
if (chunk.length < 27 + pageSegments + totalSize) return false;
let start = 27 + pageSegments;
for (const size of sizes) {
const segment = chunk.slice(start, start + size);
const header = segment.slice(0, 8);
if (this._head) {
if (header.equals(OPUS_TAGS)) this.emit('tags', segment);
else if (this._bitstream === bitstream) this.push(segment);
} else if (header.equals(OPUS_HEAD)) {
this.emit('head', segment);
this._head = segment;
this._bitstream = bitstream;
} else {
this.emit('unknownSegment', segment);
}
start += size;
}
return chunk.slice(start);
}
_destroy(err, cb) {
this._cleanup();
return cb ? cb(err) : undefined;
}
_final(cb) {
this._cleanup();
cb();
}
/**
* Cleans up the demuxer when it is no longer required.
* @private
*/
_cleanup() {
this._remainder = null;
this._head = null;
this._bitstream = null;
}
}
/**
* Emitted when the demuxer encounters the opus head.
* @event OggDemuxer#head
* @memberof opus
* @param {Buffer} segment a buffer containing the opus head data.
*/
/**
* Emitted when the demuxer encounters opus tags.
* @event OggDemuxer#tags
* @memberof opus
* @param {Buffer} segment a buffer containing the opus tags.
*/
module.exports = OggDemuxer;