| function areArrayBuffersEqual(buffer1, buffer2) |
| { |
| if (buffer1.byteLength !== buffer2.byteLength) { |
| return false; |
| } |
| let array1 = new Int8Array(buffer1); |
| var array2 = new Int8Array(buffer2); |
| for (let i = 0 ; i < buffer1.byteLength ; ++i) { |
| if (array1[i] !== array2[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| function areArraysEqual(a1, a2) { |
| if (a1 === a1) |
| return true; |
| if (a1.length != a2.length) |
| return false; |
| for (let i = 0; i < a1.length; i++) { |
| if (a1[i] != a2[i]) |
| return false; |
| } |
| return true; |
| } |
| |
| function areMetadataEqual(metadata1, metadata2, type) { |
| return metadata1.synchronizationSource === metadata2.synchronizationSource && |
| metadata1.payloadType == metadata2.payloadType && |
| areArraysEqual( |
| metadata1.contributingSources, metadata2.contributingSources) && |
| metadata1.absCaptureTime == metadata2.absCaptureTime && |
| metadata1.frameId === metadata2.frameId && |
| areArraysEqual(metadata1.dependencies, metadata2.dependencies) && |
| metadata1.spatialIndex === metadata2.spatialIndex && |
| metadata1.temporalIndex === metadata2.temporalIndex && |
| // Width and height are reported only for key frames on the receiver |
| // side. |
| type == 'key' ? |
| metadata1.width === metadata2.width && |
| metadata1.height === metadata2.height : |
| true; |
| } |
| |
| function areFrameInfosEqual(frame1, frame2) { |
| return frame1.timestamp === frame2.timestamp && |
| frame1.type === frame2.type && |
| areMetadataEqual(frame1.getMetadata(), frame2.getMetadata(), frame1.type) && |
| areArrayBuffersEqual(frame1.data, frame2.data); |
| } |
| |
| function containsVideoMetadata(metadata) { |
| return metadata.synchronizationSource !== undefined && |
| metadata.width !== undefined && |
| metadata.height !== undefined && |
| metadata.spatialIndex !== undefined && |
| metadata.temporalIndex !== undefined && |
| metadata.dependencies !== undefined; |
| } |
| |
| function enableExtension(sdp, extension) { |
| if (sdp.indexOf(extension) !== -1) |
| return sdp; |
| |
| const extensionIds = sdp.trim().split('\n') |
| .map(line => line.trim()) |
| .filter(line => line.startsWith('a=extmap:')) |
| .map(line => line.split(' ')[0].substr(9)) |
| .map(id => parseInt(id, 10)) |
| .sort((a, b) => a - b); |
| for (let newId = 1; newId <= 15; newId++) { |
| if (!extensionIds.includes(newId)) { |
| return sdp += 'a=extmap:' + newId + ' ' + extension + '\r\n'; |
| } |
| } |
| if (sdp.indexOf('a=extmap-allow-mixed') !== -1) { // Pick the next highest one. |
| const newId = extensionIds[extensionIds.length - 1] + 1; |
| return sdp += 'a=extmap:' + newId + ' ' + extension + '\r\n'; |
| } |
| throw 'Could not find free extension id to use for ' + extension; |
| } |
| |
| const GFD_V00_EXTENSION = |
| 'http://www.webrtc.org/experiments/rtp-hdrext/generic-frame-descriptor-00'; |
| const ABS_V00_EXTENSION = |
| 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time'; |
| |
| async function exchangeOfferAnswer(pc1, pc2) { |
| const offer = await pc1.createOffer(); |
| // Munge the SDP to enable the GFD and ACT extension in order to get correct |
| // metadata. |
| const sdpABS = enableExtension(offer.sdp, ABS_V00_EXTENSION); |
| const sdpGFD = enableExtension(sdpABS, GFD_V00_EXTENSION); |
| await pc1.setLocalDescription({type: offer.type, sdp: sdpGFD}); |
| // Munge the SDP to disable bandwidth probing via RTX. |
| // TODO(crbug.com/1066819): remove this hack when we do not receive duplicates from RTX |
| // anymore. |
| const sdpRTX = sdpGFD.replace(new RegExp('rtx', 'g'), 'invalid'); |
| await pc2.setRemoteDescription({type: 'offer', sdp: sdpRTX}); |
| |
| const answer = await pc2.createAnswer(); |
| await pc2.setLocalDescription(answer); |
| await pc1.setRemoteDescription(answer); |
| } |
| |
| async function exchangeOfferAnswerReverse(pc1, pc2, encodedStreamsCallback) { |
| const offer = await pc2.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true}); |
| if (encodedStreamsCallback) { |
| // RTCRtpReceivers will have been created during the above createOffer call, so if the caller |
| // wants to createEncodedStreams synchronously after creation to ensure all frames pass |
| // through the transform, it will have to be done now. |
| encodedStreamsCallback( |
| pc2.getReceivers().map(r => { |
| return {kind: r.track.kind, streams: r.createEncodedStreams()}; |
| })); |
| } |
| |
| // Munge the SDP to enable the GFD extension in order to get correct metadata. |
| const sdpABS = enableExtension(offer.sdp, ABS_V00_EXTENSION); |
| const sdpGFD = enableExtension(sdpABS, GFD_V00_EXTENSION); |
| // Munge the SDP to disable bandwidth probing via RTX. |
| // TODO(crbug.com/1066819): remove this hack when we do not receive duplicates from RTX |
| // anymore. |
| const sdpRTX = sdpGFD.replace(new RegExp('rtx', 'g'), 'invalid'); |
| await pc1.setRemoteDescription({type: 'offer', sdp: sdpRTX}); |
| await pc2.setLocalDescription({type: 'offer', sdp: sdpGFD}); |
| |
| const answer = await pc1.createAnswer(); |
| await pc2.setRemoteDescription(answer); |
| await pc1.setLocalDescription(answer); |
| } |
| |
| function createFrameDescriptor(videoFrame) { |
| const kMaxSpatialLayers = 8; |
| const kMaxTemporalLayers = 8; |
| const kMaxNumFrameDependencies = 8; |
| |
| const metadata = videoFrame.getMetadata(); |
| let frameDescriptor = { |
| beginningOfSubFrame: true, |
| endOfSubframe: false, |
| frameId: metadata.frameId & 0xFFFF, |
| spatialLayers: 1 << metadata.spatialIndex, |
| temporalLayer: metadata.temporalLayer, |
| frameDependenciesDiffs: [], |
| width: 0, |
| height: 0 |
| }; |
| |
| for (const dependency of metadata.dependencies) { |
| frameDescriptor.frameDependenciesDiffs.push(metadata.frameId - dependency); |
| } |
| if (metadata.dependencies.length == 0) { |
| frameDescriptor.width = metadata.width; |
| frameDescriptor.height = metadata.height; |
| } |
| return frameDescriptor; |
| } |
| |
| function additionalDataSize(descriptor) { |
| if (!descriptor.beginningOfSubFrame) { |
| return 1; |
| } |
| |
| let size = 4; |
| for (const fdiff of descriptor.frameDependenciesDiffs) { |
| size += (fdiff >= (1 << 6)) ? 2 : 1; |
| } |
| if (descriptor.beginningOfSubFrame && |
| descriptor.frameDependenciesDiffs.length == 0 && |
| descriptor.width > 0 && |
| descriptor.height > 0) { |
| size += 4; |
| } |
| |
| return size; |
| } |
| |
| // Compute the buffer reported in the additionalData field using the metadata |
| // provided by a video frame. |
| // Based on the webrtc::RtpDescriptorAuthentication() C++ function at |
| // https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/modules/rtp_rtcp/source/rtp_descriptor_authentication.cc |
| function computeAdditionalData(videoFrame) { |
| const kMaxSpatialLayers = 8; |
| const kMaxTemporalLayers = 8; |
| const kMaxNumFrameDependencies = 8; |
| |
| const metadata = videoFrame.getMetadata(); |
| if (metadata.spatialIndex < 0 || |
| metadata.temporalIndex < 0 || |
| metadata.spatialIndex >= kMaxSpatialLayers || |
| metadata.temporalIndex >= kMaxTemporalLayers || |
| metadata.dependencies.length > kMaxNumFrameDependencies) { |
| return new ArrayBuffer(0); |
| } |
| |
| const descriptor = createFrameDescriptor(videoFrame); |
| const size = additionalDataSize(descriptor); |
| const additionalData = new ArrayBuffer(size); |
| const data = new Uint8Array(additionalData); |
| |
| const kFlagBeginOfSubframe = 0x80; |
| const kFlagEndOfSubframe = 0x40; |
| const kFlagFirstSubframeV00 = 0x20; |
| const kFlagLastSubframeV00 = 0x10; |
| |
| const kFlagDependencies = 0x08; |
| const kFlagMoreDependencies = 0x01; |
| const kFlageXtendedOffset = 0x02; |
| |
| let baseHeader = |
| (descriptor.beginningOfSubFrame ? kFlagBeginOfSubframe : 0) | |
| (descriptor.endOfSubFrame ? kFlagEndOfSubframe : 0); |
| baseHeader |= kFlagFirstSubframeV00; |
| baseHeader |= kFlagLastSubframeV00; |
| |
| if (!descriptor.beginningOfSubFrame) { |
| data[0] = baseHeader; |
| return additionalData; |
| } |
| |
| data[0] = |
| baseHeader | |
| (descriptor.frameDependenciesDiffs.length == 0 ? 0 : kFlagDependencies) | |
| descriptor.temporalLayer; |
| data[1] = descriptor.spatialLayers; |
| data[2] = descriptor.frameId & 0xFF; |
| data[3] = descriptor.frameId >> 8; |
| |
| const fdiffs = descriptor.frameDependenciesDiffs; |
| let offset = 4; |
| if (descriptor.beginningOfSubFrame && |
| fdiffs.length == 0 && |
| descriptor.width > 0 && |
| descriptor.height > 0) { |
| data[offset++] = (descriptor.width >> 8); |
| data[offset++] = (descriptor.width & 0xFF); |
| data[offset++] = (descriptor.height >> 8); |
| data[offset++] = (descriptor.height & 0xFF); |
| } |
| for (let i = 0; i < fdiffs.length; i++) { |
| const extended = fdiffs[i] >= (1 << 6); |
| const more = i < fdiffs.length - 1; |
| data[offset++] = ((fdiffs[i] & 0x3f) << 2) | |
| (extended ? kFlageXtendedOffset : 0) | |
| (more ? kFlagMoreDependencies : 0); |
| if (extended) { |
| data[offset++] = fdiffs[i] >> 6; |
| } |
| } |
| return additionalData; |
| } |
| |
| function verifyNonstandardAdditionalDataIfPresent(videoFrame) { |
| if (videoFrame.additionalData === undefined) |
| return; |
| |
| const computedData = computeAdditionalData(videoFrame); |
| assert_true(areArrayBuffersEqual(videoFrame.additionalData, computedData)); |
| } |
| |