-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathStreamer.ts
332 lines (304 loc) · 11.6 KB
/
Streamer.ts
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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
/**
* Copyright 2023 Ceeblue B.V.
* This file is part of https://github.com/CeeblueTV/webrtc-client which is released under GNU Affero General Public License.
* See file LICENSE or go to https://spdx.org/licenses/AGPL-3.0-or-later.html for full license details.
*/
import { Util, ILog, Connect, EventEmitter, WebSocketReliableError } from '@ceeblue/web-utils';
import { ConnectionInfos, ConnectorError, IConnector } from './connectors/IConnector';
import { WSController } from './connectors/WSController';
import { HTTPConnector } from './connectors/HTTPConnector';
import { IController, IsController, RTPProps, MediaReport } from './connectors/IController';
import { ABRAbstract, ABRParams } from './abr/ABRAbstract';
import { ABRLinear } from './abr/ABRLinear';
export type StreamerError =
/**
* Represents a {@link ConnectorError} error
*/
| ConnectorError
/**
* Represents a {@link WebSocketReliableError} error
*/
| WebSocketReliableError;
/**
* Use Streamer to broadcast to a WebRTC server.
*
* You can use a controllable version using a `WSController` as connector, or change it to use a `HTTPConnector` (HTTP WHIP).
* By default it uses a `WSController` excepting if on {@link Streamer.start} you use a {@link Connect.Params.endPoint} prefixed with a `http://` protocol.
* With a controllable connector you can change video bitrate during the streaming, what is not possible with a HTTP(WHIP) connector.
*
* @example
* const streamer = new Streamer();
* // const streamer = new Streamer(isWHEP ? HTTPConnector : WSController);
* streamer.onStart = stream => {
* console.log('start streaming');
* }
* streamer.onStop = _ => {
* console.log('stop streaming');
* }
* navigator.mediaDevices
* .getUserMedia({ audio: true, video: true })
* .then(stream => {
* streamer.start(stream, {
* endPoint: address, // if address is prefixed by `http://` it uses a HTTPConnector (HTTP-WHIP) if Streamer is build without contructor argument
* streamName: 'as+bc3f535f-37f3-458b-8171-b4c5e77a6137'
* });
* ...
* streamer.stop();
* });
*/
export class Streamer extends EventEmitter {
/**
* Event fired when the stream has started
* @param stream
* @event
*/
onStart(stream: MediaStream) {
this.log('onStart').info();
}
/**
* Event fired when the stream has stopped
* @param error error description on an improper stop
* @event
*/
onStop(error?: StreamerError) {
if (error) {
this.log('onStop', error).error();
} else {
this.log('onStop').info();
}
}
/**
* Event fired when an RTP setting change occurs
* @param props
*/
onRTPProps(props: RTPProps) {}
/**
* Event fired to report media statistics
* @param mediaReport
*/
onMediaReport(mediaReport: MediaReport) {}
/**
* Event fired when a video bitrate change occurs
* @param videoBitrate
* @param videoBitrateConstraint
*/
onVideoBitrate(videoBitrate: number, videoBitrateConstraint: number) {
this.log(`onVideoBitrate ${Util.stringify({ videoBitrate, videoBitrateConstraint })}`).info();
}
/**
* Stream name, for example `as+bc3f535f-37f3-458b-8171-b4c5e77a6137`
*/
get streamName(): string {
return this._connector ? this._connector.streamName : '';
}
/**
* Camera media stream as specified by [MediaStream](https://developer.mozilla.org/docs/Web/API/MediaStream)
*/
get stream(): MediaStream | undefined {
return this._connector && this._connector.stream;
}
/**
* Returns true when streamer is running (between a {@link Streamer.start} and a {@link Streamer.stop})
*/
get running(): boolean {
return this._connector ? true : false;
}
/**
* Returns the {@link IController} instance when starting with a connector with controllable ability,
* or undefined if stream is not starting or stream is not controllable.
*/
get controller(): IController | undefined {
return this._controller;
}
/**
* Returns the {@link IConnector} instance, or undefined if stream is not starting.
*/
get connector(): IConnector | undefined {
return this._connector;
}
/**
* Last {@link MediaReport} statistics
*/
get mediaReport(): MediaReport | undefined {
return this._mediaReport;
}
/**
* Last {@link RTPProps} statistics
*/
get rtpProps(): RTPProps | undefined {
return this._rtpProps;
}
/**
* Video bitrate configured by the server,
* can be undefined on start or when there is no controllable connector
* @note Use {@link connectionInfos} to get the current precise audio or video bitrate
*/
get videoBitrate(): number | undefined {
return this._videoBitrate;
}
/**
* Configure the video bitrate from the server,
* possible only if your {@link Streamer} instance is built with a controllable connector
* Set undefined to remove this configuration
*/
set videoBitrate(value: number | undefined) {
if (value === this._videoBitrate) {
return;
}
if (!this._controller) {
throw Error('Cannot set videoBitrate without start a controllable session');
}
if (value == null) {
this._videoBitrateFixed = false;
return;
}
this._videoBitrateFixed = true;
this._videoBitrate = value;
this._controller.setVideoBitrate(value);
}
/**
* Video bitrate constraint configured by the server,
* can be undefined on start or when there is no controllable connector
* @note Use {@link connectionInfos} to get the current precise audio or video bitrate
*/
get videoBitrateConstraint(): number | undefined {
return this._videoBitrateConstraint;
}
private _connector?: IConnector;
private _controller?: IController;
private _mediaReport?: MediaReport;
private _videoBitrate?: number;
private _videoBitrateConstraint?: number;
private _videoBitrateFixed: boolean;
private _rtpProps?: RTPProps;
/**
* Constructs a new Streamer instance, optionally with a custom connector
* This doesn't start the broadcast, you must call start() method
* @param Connector Connector class to use for signaling, can be determined automatically from URL in the start() method
*/
constructor(private Connector?: { new (connectParams: Connect.Params, stream: MediaStream): IConnector }) {
super();
this._videoBitrateFixed = false;
}
/**
* Sets server properties for packet error (nack) and delayed packet loss (drop)
* and fires an onRTPProps event if changed successfully.
* NOTE: Method can also retrieve current server values if called without arguments.
* @param nack Waiting period before declaring a packet error
* @param drop Waiting period before considering delayed packets as lost
*/
setRTPProps(nack?: number, drop?: number) {
if (!this._controller) {
throw Error('Cannot set rtpProps without start a controllable session');
}
this._controller.setRTPProps(nack, drop);
}
/**
* Returns connection info, such as round trip time, requests sent and received,
* bytes sent and received, and bitrates
* NOTE: This call is resource-intensive for the CPU.
* @returns {Promise<ConnectionInfos>} A promise for a ConnectionInfos
*/
connectionInfos(): Promise<ConnectionInfos> {
if (!this._connector) {
return Promise.reject('Start streamer before to request connection infos');
}
return this._connector.connectionInfos();
}
/**
* Starts broadcasting the stream
* The connector is determined automatically from {@link Connect.Params.endPoint} if not forced in the constructor.
*
* The `adaptiveBitrate` option can take three different types of value:
* - A {@link ABRParams} parameters to configure the default ABRLinear implementation
* - undefined to disable ABR management
* - Use a custom {@link ABRAbstract} implementation instance
*
* @param stream {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaStream MediaStream} instance to stream
* @param params Connection parameters
* @param adaptiveBitrate Adaptive bitrate implementation or ABRParams to configure the default implementation
*/
start(stream: MediaStream, params: Connect.Params, adaptiveBitrate: ABRAbstract | ABRParams | undefined = {}) {
this.stop();
// Connector
let abr: ABRAbstract;
this._videoBitrateFixed = false;
this._connector = new (this.Connector || (params.endPoint.startsWith('http') ? HTTPConnector : WSController))(
params,
stream
);
this._connector.log = this.log.bind(this, 'Signaling:') as ILog;
this._connector.onOpen = stream => this.onStart(stream);
this._connector.onClose = (error?: ConnectorError) => {
// reset to release resources!
abr?.reset();
// Stop the streamer if signaling fails!
this.stop(error);
};
if (!IsController(this._connector)) {
if (adaptiveBitrate) {
this.log(
`Cannot use an adaptive bitrate without a controller: Connector ${this._connector.constructor.name} doesn't implement IController`
).error();
}
return;
}
// Controller
if (adaptiveBitrate) {
if ('compute' in adaptiveBitrate) {
abr = adaptiveBitrate;
} else {
abr = new ABRLinear(adaptiveBitrate);
abr.log = this.log.bind(this, 'AdaptiveBitrate:') as ILog;
}
}
this._controller = this._connector;
this._controller.onOpen = stream => {
this._computeVideoBitrate(abr);
this.onStart(stream);
};
this._controller.onRTPProps = props => {
this._rtpProps = props;
this.onRTPProps(props);
};
this._controller.onMediaReport = async mediaReport => {
this._mediaReport = mediaReport;
this._computeVideoBitrate(abr);
this.onMediaReport(mediaReport);
};
this._controller.onVideoBitrate = (video_bitrate, video_bitrate_constraint) => {
this._videoBitrate = video_bitrate;
this._videoBitrateConstraint = video_bitrate_constraint;
this.onVideoBitrate(video_bitrate, video_bitrate_constraint);
};
}
/**
* Stop streaming the stream
* @param error error description on an improper stop
*/
stop(error?: StreamerError) {
const connector = this._connector;
if (!connector) {
return;
}
this._connector = undefined;
connector.close();
this._controller = undefined;
this._mediaReport = undefined;
this._videoBitrate = undefined;
this._videoBitrateConstraint = undefined;
this._rtpProps = undefined;
// User event (always in last)
this.onStop(error);
}
private _computeVideoBitrate(abr: ABRAbstract) {
if (!this._controller || this._videoBitrateFixed) {
return;
}
const videoBitrate = abr.compute(this._videoBitrate, this.videoBitrateConstraint, this.mediaReport);
if (videoBitrate !== this._videoBitrate) {
this._videoBitrate = videoBitrate;
this._controller.setVideoBitrate(videoBitrate);
}
}
}