@epicgames-ps/lib-pixelstreamingfrontend-ue5.4
Version:
Frontend library for Unreal Engine 5.4 Pixel Streaming
625 lines (479 loc) • 30.2 kB
text/typescript
import { mockRTCRtpReceiver, unmockRTCRtpReceiver } from '../__test__/mockRTCRtpReceiver';
import {
Config,
NumericParameters,
} from '../Config/Config';
import { PixelStreaming } from './PixelStreaming';
import { SettingsChangedEvent, StreamerListMessageEvent, WebRtcConnectedEvent, WebRtcSdpEvent } from '../Util/EventEmitter';
import { mockWebSocket, MockWebSocketSpyFunctions, MockWebSocketTriggerFunctions, unmockWebSocket } from '../__test__/mockWebSocket';
import { MessageRecvTypes } from '../WebSockets/MessageReceive';
import { mockRTCPeerConnection, MockRTCPeerConnectionSpyFunctions, MockRTCPeerConnectionTriggerFunctions, unmockRTCPeerConnection } from '../__test__/mockRTCPeerConnection';
import { mockHTMLMediaElement, mockMediaStream, unmockMediaStream } from '../__test__/mockMediaStream';
import { InitialSettings } from '../DataChannel/InitialSettings';
const flushPromises = () => new Promise(jest.requireActual("timers").setImmediate);
describe('PixelStreaming', () => {
let webSocketSpyFunctions: MockWebSocketSpyFunctions;
let webSocketTriggerFunctions: MockWebSocketTriggerFunctions;
let rtcPeerConnectionSpyFunctions: MockRTCPeerConnectionSpyFunctions;
let rtcPeerConnectionTriggerFunctions: MockRTCPeerConnectionTriggerFunctions;
const mockSignallingUrl = 'ws://localhost:24680/';
const streamerId = "MOCK_PIXEL_STREAMING";
const streamerIdList = [streamerId];
const sdp = "v=0\r\no=- 974006863270230083 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1 2\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS pixelstreaming_audio_stream_id pixelstreaming_video_stream_id\r\nm=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:+JE1\r\na=ice-pwd:R2dKmHqM47E++7TRKKkHMyHj\r\na=ice-options:trickle\r\na=fingerprint:sha-256 20:EE:85:F0:DA:F4:90:F3:0D:13:2E:A9:1E:36:8C:81:E1:BD:38:78:20:AA:38:F3:FC:65:3F:8E:06:1D:A7:53\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 urn:3gpp:video-orientation\r\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendonly\r\na=msid:pixelstreaming_video_stream_id pixelstreaming_video_track_label\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 H264/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:98 H264/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:100 red/90000\r\na=rtpmap:101 rtx/90000\r\na=fmtp:101 apt=100\r\na=rtpmap:102 ulpfec/90000\r\na=ssrc-group:FID 3702690738 1574960745\r\na=ssrc:3702690738 cname:I/iLZxsY4mZ0aoNG\r\na=ssrc:3702690738 msid:pixelstreaming_video_stream_id pixelstreaming_video_track_label\r\na=ssrc:3702690738 mslabel:pixelstreaming_video_stream_id\r\na=ssrc:3702690738 label:pixelstreaming_video_track_label\r\na=ssrc:1574960745 cname:I/iLZxsY4mZ0aoNG\r\na=ssrc:1574960745 msid:pixelstreaming_video_stream_id pixelstreaming_video_track_label\r\na=ssrc:1574960745 mslabel:pixelstreaming_video_stream_id\r\na=ssrc:1574960745 label:pixelstreaming_video_track_label\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111 63 110\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:+JE1\r\na=ice-pwd:R2dKmHqM47E++7TRKKkHMyHj\r\na=ice-options:trickle\r\na=fingerprint:sha-256 20:EE:85:F0:DA:F4:90:F3:0D:13:2E:A9:1E:36:8C:81:E1:BD:38:78:20:AA:38:F3:FC:65:3F:8E:06:1D:A7:53\r\na=setup:actpass\r\na=mid:1\r\na=extmap:14 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendrecv\r\na=msid:pixelstreaming_audio_stream_id pixelstreaming_audio_track_label\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=rtcp-fb:111 transport-cc\r\na=fmtp:111 maxaveragebitrate=510000;maxplaybackrate=48000;minptime=3;sprop-stereo=1;stereo=1;usedtx=0;useinbandfec=1\r\na=rtpmap:63 red/48000/2\r\na=fmtp:63 111/111\r\na=rtpmap:110 telephone-event/48000\r\na=maxptime:120\r\na=ptime:20\r\na=ssrc:2587776314 cname:I/iLZxsY4mZ0aoNG\r\na=ssrc:2587776314 msid:pixelstreaming_audio_stream_id pixelstreaming_audio_track_label\r\na=ssrc:2587776314 mslabel:pixelstreaming_audio_stream_id\r\na=ssrc:2587776314 label:pixelstreaming_audio_track_label\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 0.0.0.0\r\na=ice-ufrag:+JE1\r\na=ice-pwd:R2dKmHqM47E++7TRKKkHMyHj\r\na=ice-options:trickle\r\na=fingerprint:sha-256 20:EE:85:F0:DA:F4:90:F3:0D:13:2E:A9:1E:36:8C:81:E1:BD:38:78:20:AA:38:F3:FC:65:3F:8E:06:1D:A7:53\r\na=setup:actpass\r\na=mid:2\r\na=sctp-port:5000\r\na=max-message-size:262144\r\n";
const iceCandidate: RTCIceCandidateInit = {
sdpMid: "0",
sdpMLineIndex: null,
usernameFragment: null,
candidate:"candidate:2199032595 1 udp 2122260223 192.168.1.89 64674 typ host generation 0 ufrag +JE1 network-id 1"
};
const triggerWebSocketOpen = () =>
webSocketTriggerFunctions.triggerOnOpen?.();
const triggerConfigMessage = () =>
webSocketTriggerFunctions.triggerOnMessage?.({
type: MessageRecvTypes.CONFIG,
peerConnectionOptions: {}
});
const triggerStreamerListMessage = (streamerIdList: string[]) =>
webSocketTriggerFunctions.triggerOnMessage?.({
type: MessageRecvTypes.STREAMER_LIST,
ids: streamerIdList
});
const triggerSdpOfferMessage = () =>
webSocketTriggerFunctions.triggerOnMessage?.({
type: MessageRecvTypes.OFFER,
sdp
});
const triggerIceCandidateMessage = () =>
webSocketTriggerFunctions.triggerOnMessage?.({
type: MessageRecvTypes.ICE_CANDIDATE,
candidate: iceCandidate
});
const triggerIceConnectionState = (state: RTCIceConnectionState) =>
rtcPeerConnectionTriggerFunctions.triggerIceConnectionStateChange?.(
state
);
const triggerAddTrack = () => {
const stream = new MediaStream();
const track = new MediaStreamTrack();
rtcPeerConnectionTriggerFunctions.triggerOnTrack?.({
track,
streams: [stream]
} as RTCTrackEventInit);
return { stream, track };
};
const triggerOpenDataChannel = () => {
const channel = new RTCDataChannel();
rtcPeerConnectionTriggerFunctions.triggerOnDataChannel?.({
channel
});
channel.onopen?.(new Event('open'));
return { channel };
};
const establishMockedPixelStreamingConnection = (
streamerIds = streamerIdList,
iceConnectionState: RTCIceConnectionState = 'connected'
) => {
triggerWebSocketOpen();
triggerConfigMessage();
triggerStreamerListMessage(streamerIds);
triggerSdpOfferMessage();
triggerIceCandidateMessage();
triggerIceConnectionState(iceConnectionState);
const { stream, track } = triggerAddTrack();
const { channel } = triggerOpenDataChannel();
return { channel, stream, track };
};
beforeEach(() => {
mockRTCRtpReceiver();
mockMediaStream();
[webSocketSpyFunctions, webSocketTriggerFunctions] = mockWebSocket();
[rtcPeerConnectionSpyFunctions, rtcPeerConnectionTriggerFunctions] = mockRTCPeerConnection();
mockHTMLMediaElement({ ableToPlay: true });
jest.useFakeTimers();
});
afterEach(() => {
unmockRTCRtpReceiver();
unmockMediaStream();
unmockWebSocket();
unmockRTCPeerConnection();
jest.resetAllMocks();
});
it('should emit settingsChanged events when the configuration is updated', () => {
const config = new Config();
const pixelStreaming = new PixelStreaming(config);
const settingsChangedSpy = jest.fn();
pixelStreaming.addEventListener("settingsChanged", settingsChangedSpy);
expect(settingsChangedSpy).not.toHaveBeenCalled();
config.setNumericSetting(NumericParameters.WebRTCMaxBitrate, 123);
expect(settingsChangedSpy).toHaveBeenCalledWith(new SettingsChangedEvent({
id: NumericParameters.WebRTCMaxBitrate,
target: config.getNumericSettings().find((setting) => setting.id === NumericParameters.WebRTCMaxBitrate)!,
type: 'number',
value: 123,
}));
});
it('should connect to signalling server when connect is called', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const pixelStreaming = new PixelStreaming(config);
expect(webSocketSpyFunctions.constructorSpy).not.toHaveBeenCalled();
pixelStreaming.connect();
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
});
it('should autoconnect to signalling server if autoconnect setting is enabled', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
expect(webSocketSpyFunctions.constructorSpy).not.toHaveBeenCalled();
const pixelStreaming = new PixelStreaming(config);
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
});
it('should disconnect from signalling server if disconnect is called', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
const disconnectedSpy = jest.fn();
expect(webSocketSpyFunctions.constructorSpy).not.toHaveBeenCalled();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("webRtcDisconnected", disconnectedSpy);
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled();
pixelStreaming.disconnect();
expect(webSocketSpyFunctions.closeSpy).toHaveBeenCalled();
expect(disconnectedSpy).toHaveBeenCalled();
});
it('should connect immediately to signalling server if reconnect is called and connection is not up', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const pixelStreaming = new PixelStreaming(config);
expect(webSocketSpyFunctions.constructorSpy).not.toHaveBeenCalled();
pixelStreaming.reconnect();
expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled();
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
});
it('should disconnect and reconnect to signalling server if reconnect is called and connection is up', () => {
// We explicitly set the max reconnect attempts to 0 to stop the auto-reconnect flow as that is tested separate
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true, MaxReconnectAttempts: 0}});
const autoconnectedSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("webRtcAutoConnect", autoconnectedSpy);
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(1);
expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled();
pixelStreaming.reconnect();
expect(webSocketSpyFunctions.closeSpy).toHaveBeenCalled();
// delayed reconnect after 3 seconds
jest.advanceTimersByTime(3000);
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(2);
expect(autoconnectedSpy).toHaveBeenCalled();
});
it('should automatically reconnect and request streamer list N times on websocket close', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true, MaxReconnectAttempts: 3}});
const autoconnectedSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("webRtcAutoConnect", autoconnectedSpy);
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(1);
expect(webSocketSpyFunctions.closeSpy).not.toHaveBeenCalled();
pixelStreaming.webSocketController.close();
expect(webSocketSpyFunctions.closeSpy).toHaveBeenCalled();
// wait 2 seconds
jest.advanceTimersByTime(2000);
// we should have attempted a reconnection
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledWith(mockSignallingUrl);
expect(webSocketSpyFunctions.constructorSpy).toHaveBeenCalledTimes(2);
// Reconnect triggers the first list streamer message
triggerWebSocketOpen();
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith(
expect.stringMatching(/"type":"listStreamers"/)
);
// We don't have a signalling server to respond with data so lets just fake a response with no streamers
triggerStreamerListMessage([]);
// Wait 2 seconds. This delay waits for the WebRtcPlayerController to realise the previously received list doesn't contain
// the streamer is was preiously subscribed to, so it'll request the list again
jest.advanceTimersByTime(2000);
// Same as above but repeated for the second call
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith(
expect.stringMatching(/"type":"listStreamers"/)
);
triggerStreamerListMessage([]);
jest.advanceTimersByTime(2000);
// Expect the third call
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith(
expect.stringMatching(/"type":"listStreamers"/)
);
triggerStreamerListMessage([]);
jest.advanceTimersByTime(2000);
// We should expect only 3 calls based on our config
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledTimes(3);
});
it('should request streamer list when connected to the signalling server', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
const pixelStreaming = new PixelStreaming(config);
triggerWebSocketOpen();
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith(
expect.stringMatching(/"type":"listStreamers"/)
);
});
it('should autoselect a streamer if receiving only one streamer in streamerList message', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
const streamerListSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("streamerListMessage", streamerListSpy);
triggerWebSocketOpen();
triggerConfigMessage();
triggerStreamerListMessage(streamerIdList);
expect(streamerListSpy).toHaveBeenCalledWith(new StreamerListMessageEvent({
messageStreamerList: expect.objectContaining({
type: MessageRecvTypes.STREAMER_LIST,
ids: streamerIdList
}),
autoSelectedStreamerId: streamerId,
wantedStreamerId: null
}));
expect(webSocketSpyFunctions.sendSpy).toHaveBeenCalledWith(
expect.stringMatching(/"type":"subscribe".*MOCK_PIXEL_STREAMING/)
);
});
it('should not autoselect a streamer if receiving multiple streamers in streamerList message', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
const streamerId2 = "MOCK_2_PIXEL_STREAMING";
const extendedStreamerIdList = [streamerId, streamerId2];
const streamerListSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("streamerListMessage", streamerListSpy);
triggerWebSocketOpen();
triggerConfigMessage();
triggerStreamerListMessage(extendedStreamerIdList);
expect(streamerListSpy).toHaveBeenCalledWith(new StreamerListMessageEvent({
messageStreamerList: expect.objectContaining({
type: MessageRecvTypes.STREAMER_LIST,
ids: extendedStreamerIdList
}),
autoSelectedStreamerId: null,
wantedStreamerId: null
}));
expect(webSocketSpyFunctions.sendSpy).not.toHaveBeenCalledWith(
expect.stringMatching(/"type":"subscribe"/)
);
});
it('should set remoteDescription and emit webRtcSdp event when an offer is received', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
const eventSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("webRtcSdp", eventSpy);
triggerWebSocketOpen();
triggerConfigMessage();
triggerStreamerListMessage(streamerIdList);
expect(eventSpy).not.toHaveBeenCalled();
triggerSdpOfferMessage();
expect(rtcPeerConnectionSpyFunctions.setRemoteDescriptionSpy).toHaveBeenCalledWith(expect.objectContaining({
sdp
}));
expect(eventSpy).toHaveBeenCalledWith(new WebRtcSdpEvent());
});
it('should add an ICE candidate when receiving a iceCandidate message', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
const pixelStreaming = new PixelStreaming(config);
triggerWebSocketOpen();
triggerConfigMessage();
triggerStreamerListMessage(streamerIdList);
triggerSdpOfferMessage();
triggerIceCandidateMessage();
expect(rtcPeerConnectionSpyFunctions.addIceCandidateSpy).toHaveBeenCalledWith(iceCandidate)
});
it('should emit webRtcConnected event when ICE connection state is connected', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
const connectedSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("webRtcConnected", connectedSpy);
triggerWebSocketOpen();
expect(rtcPeerConnectionSpyFunctions.constructorSpy).not.toHaveBeenCalled();
triggerConfigMessage();
expect(rtcPeerConnectionSpyFunctions.constructorSpy).toHaveBeenCalled();
triggerIceCandidateMessage();
triggerIceConnectionState('connected')
expect(connectedSpy).toHaveBeenCalledWith(new WebRtcConnectedEvent());
});
it('should call RTCPeerConnection close and emit webRtcDisconnected when disconnect is called', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const disconnectedSpy = jest.fn();
const dataChannelSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("webRtcDisconnected", disconnectedSpy);
pixelStreaming.addEventListener("dataChannelClose", dataChannelSpy);
pixelStreaming.connect();
establishMockedPixelStreamingConnection();
pixelStreaming.disconnect();
expect(rtcPeerConnectionSpyFunctions.closeSpy).toHaveBeenCalled();
expect(disconnectedSpy).toHaveBeenCalled();
expect(dataChannelSpy).toHaveBeenCalled();
});
it('should emit statistics when connected', async () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl, AutoConnect: true}});
const statsSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("statsReceived", statsSpy);
establishMockedPixelStreamingConnection();
expect(statsSpy).not.toHaveBeenCalled();
// New stats sent at 1s intervals
jest.advanceTimersByTime(1000);
await flushPromises();
expect(statsSpy).toHaveBeenCalledTimes(1);
expect(statsSpy).toHaveBeenCalledWith(
expect.objectContaining({
data: {
aggregatedStats: expect.objectContaining({
candidatePairs: [
expect.objectContaining({ bytesReceived: 123 })
],
localCandidates: [
expect.objectContaining({ address: 'mock-address' })
]
})
}
})
);
jest.advanceTimersByTime(1000);
await flushPromises();
expect(statsSpy).toHaveBeenCalledTimes(2);
});
it('should emit dataChannelOpen when data channel is opened', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const dataChannelSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("dataChannelOpen", dataChannelSpy);
pixelStreaming.connect();
establishMockedPixelStreamingConnection();
expect(dataChannelSpy).toHaveBeenCalled();
});
it('should emit playStream when video play is called', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const streamSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("playStream", streamSpy);
pixelStreaming.connect();
establishMockedPixelStreamingConnection();
pixelStreaming.play();
expect(streamSpy).toHaveBeenCalled();
});
it('should emit playStreamRejected if video play is rejected', async () => {
mockHTMLMediaElement({ ableToPlay: false });
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const streamRejectedSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("playStreamRejected", streamRejectedSpy);
pixelStreaming.connect();
establishMockedPixelStreamingConnection();
pixelStreaming.play();
await flushPromises();
expect(streamRejectedSpy).toHaveBeenCalled();
});
it('should send data through the data channel when emitCommand is called', () => {
mockHTMLMediaElement({ ableToPlay: true, readyState: 2 });
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.connect();
establishMockedPixelStreamingConnection();
pixelStreaming.play();
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled();
const commandSent = pixelStreaming.emitCommand({
'Resolution.Width': 123,
'Resolution.Height': 456
});
expect(commandSent).toEqual(true);
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).toHaveBeenCalled();
});
it('should prevent sending console commands unless permitted by streamer', () => {
mockHTMLMediaElement({ ableToPlay: true, readyState: 2 });
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.connect();
establishMockedPixelStreamingConnection();
pixelStreaming.play();
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled();
const commandSent = pixelStreaming.emitConsoleCommand("console command");
expect(commandSent).toEqual(false);
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled();
});
it('should allow sending console commands if permitted by streamer', () => {
mockHTMLMediaElement({ ableToPlay: true, readyState: 2 });
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const initialSettingsSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("initialSettings", initialSettingsSpy);
pixelStreaming.connect();
establishMockedPixelStreamingConnection();
pixelStreaming.play();
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled();
expect(initialSettingsSpy).not.toHaveBeenCalled();
const initialSettings = new InitialSettings();
initialSettings.PixelStreamingSettings.AllowPixelStreamingCommands = true;
pixelStreaming._onInitialSettings(initialSettings);
expect(initialSettingsSpy).toHaveBeenCalled();
const commandSent = pixelStreaming.emitConsoleCommand("console command");
expect(commandSent).toEqual(true);
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).toHaveBeenCalled();
});
it('should send data through the data channel when emitUIInteraction is called', () => {
mockHTMLMediaElement({ ableToPlay: true, readyState: 2 });
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.connect();
establishMockedPixelStreamingConnection();
pixelStreaming.play();
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).not.toHaveBeenCalled();
const commandSent = pixelStreaming.emitUIInteraction({ custom: "descriptor" });
expect(commandSent).toEqual(true);
expect(rtcPeerConnectionSpyFunctions.sendDataSpy).toHaveBeenCalled();
});
it('should call user-provided callback if receiving a data channel Response message from the streamer', () => {
mockHTMLMediaElement({ ableToPlay: true, readyState: 2 });
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const responseListenerSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addResponseEventListener('responseListener', responseListenerSpy);
pixelStreaming.connect();
const { channel } = establishMockedPixelStreamingConnection();
pixelStreaming.play();
expect(responseListenerSpy).not.toHaveBeenCalled();
const testMessageContents = JSON.stringify({ test: "mock-data" });
const data = new DataView(new ArrayBuffer(1 + 2 * testMessageContents.length));
data.setUint8(0, 1); // type 1 == Response
let byteIdx = 1;
for (let i = 0; i < testMessageContents.length; i++) {
data.setUint16(byteIdx, testMessageContents.charCodeAt(i), true);
byteIdx += 2;
}
channel.dispatchEvent(new MessageEvent('message', { data: data.buffer }));
expect(responseListenerSpy).toHaveBeenCalledWith(testMessageContents);
});
it('should emit StreamConnectEvent when streamer connects', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const streamConnectSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("streamConnect", streamConnectSpy);
pixelStreaming.connect();
establishMockedPixelStreamingConnection();
expect(streamConnectSpy).toHaveBeenCalled();
});
it('should emit StreamDisconnectEvent when streamer disconnects', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const streamDisconnectSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("streamDisconnect", streamDisconnectSpy);
pixelStreaming.connect();
establishMockedPixelStreamingConnection();
expect(streamDisconnectSpy).not.toHaveBeenCalled();
pixelStreaming.disconnect();
expect(streamDisconnectSpy).toHaveBeenCalled();
});
it('should emit StreamReconnectEvent when streamer reconnects', () => {
const config = new Config({ initialSettings: {ss: mockSignallingUrl}});
const streamReconnectSpy = jest.fn();
const pixelStreaming = new PixelStreaming(config);
pixelStreaming.addEventListener("streamReconnect", streamReconnectSpy);
pixelStreaming.connect();
establishMockedPixelStreamingConnection();
expect(streamReconnectSpy).not.toHaveBeenCalled();
pixelStreaming.reconnect();
expect(streamReconnectSpy).toHaveBeenCalled();
pixelStreaming.disconnect();
expect(streamReconnectSpy).toHaveBeenCalledTimes(1);
});
});