RTCPeerConnection-insertable-streams.js (9149B)
1 function areArrayBuffersEqual(buffer1, buffer2) 2 { 3 if (buffer1.byteLength !== buffer2.byteLength) { 4 return false; 5 } 6 let array1 = new Int8Array(buffer1); 7 var array2 = new Int8Array(buffer2); 8 for (let i = 0 ; i < buffer1.byteLength ; ++i) { 9 if (array1[i] !== array2[i]) { 10 return false; 11 } 12 } 13 return true; 14 } 15 16 function areArraysEqual(a1, a2) { 17 if (a1 === a1) 18 return true; 19 if (a1.length != a2.length) 20 return false; 21 for (let i = 0; i < a1.length; i++) { 22 if (a1[i] != a2[i]) 23 return false; 24 } 25 return true; 26 } 27 28 function areMetadataEqual(metadata1, metadata2, type) { 29 return metadata1.synchronizationSource === metadata2.synchronizationSource && 30 metadata1.payloadType == metadata2.payloadType && 31 areArraysEqual( 32 metadata1.contributingSources, metadata2.contributingSources) && 33 metadata1.captureTime == metadata2.captureTime && 34 metadata1.frameId === metadata2.frameId && 35 areArraysEqual(metadata1.dependencies, metadata2.dependencies) && 36 metadata1.spatialIndex === metadata2.spatialIndex && 37 metadata1.temporalIndex === metadata2.temporalIndex && 38 // Width and height are reported only for key frames on the receiver 39 // side. 40 type == 'key' ? 41 metadata1.width === metadata2.width && 42 metadata1.height === metadata2.height : 43 true; 44 } 45 46 function areFrameInfosEqual(frame1, frame2) { 47 return frame1.timestamp === frame2.timestamp && 48 frame1.type === frame2.type && 49 areMetadataEqual(frame1.getMetadata(), frame2.getMetadata(), frame1.type) && 50 areArrayBuffersEqual(frame1.data, frame2.data); 51 } 52 53 function containsVideoMetadata(metadata) { 54 return metadata.synchronizationSource !== undefined && 55 metadata.width !== undefined && 56 metadata.height !== undefined && 57 metadata.spatialIndex !== undefined && 58 metadata.temporalIndex !== undefined && 59 metadata.dependencies !== undefined; 60 } 61 62 function enableExtension(sdp, extension) { 63 if (sdp.indexOf(extension) !== -1) 64 return sdp; 65 66 const extensionIds = sdp.trim().split('\n') 67 .map(line => line.trim()) 68 .filter(line => line.startsWith('a=extmap:')) 69 .map(line => line.split(' ')[0].substr(9)) 70 .map(id => parseInt(id, 10)) 71 .sort((a, b) => a - b); 72 for (let newId = 1; newId <= 15; newId++) { 73 if (!extensionIds.includes(newId)) { 74 return sdp += 'a=extmap:' + newId + ' ' + extension + '\r\n'; 75 } 76 } 77 if (sdp.indexOf('a=extmap-allow-mixed') !== -1) { // Pick the next highest one. 78 const newId = extensionIds[extensionIds.length - 1] + 1; 79 return sdp += 'a=extmap:' + newId + ' ' + extension + '\r\n'; 80 } 81 throw 'Could not find free extension id to use for ' + extension; 82 } 83 84 const GFD_V00_EXTENSION = 85 'http://www.webrtc.org/experiments/rtp-hdrext/generic-frame-descriptor-00'; 86 const ABS_V00_EXTENSION = 87 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time'; 88 89 async function exchangeOfferAnswer(pc1, pc2) { 90 const offer = await pc1.createOffer(); 91 // Munge the SDP to enable the GFD and ACT extension in order to get correct 92 // metadata. 93 const sdpABS = enableExtension(offer.sdp, ABS_V00_EXTENSION); 94 const sdpGFD = enableExtension(sdpABS, GFD_V00_EXTENSION); 95 await pc1.setLocalDescription({type: offer.type, sdp: sdpGFD}); 96 // Munge the SDP to disable bandwidth probing via RTX. 97 // TODO(crbug.com/1066819): remove this hack when we do not receive duplicates from RTX 98 // anymore. 99 const sdpRTX = sdpGFD.replace(new RegExp('rtx', 'g'), 'invalid'); 100 await pc2.setRemoteDescription({type: 'offer', sdp: sdpRTX}); 101 102 const answer = await pc2.createAnswer(); 103 await pc2.setLocalDescription(answer); 104 await pc1.setRemoteDescription(answer); 105 } 106 107 async function exchangeOfferAnswerReverse(pc1, pc2, encodedStreamsCallback) { 108 const offer = await pc2.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true}); 109 if (encodedStreamsCallback) { 110 // RTCRtpReceivers will have been created during the above createOffer call, so if the caller 111 // wants to createEncodedStreams synchronously after creation to ensure all frames pass 112 // through the transform, it will have to be done now. 113 encodedStreamsCallback( 114 pc2.getReceivers().map(r => { 115 return {kind: r.track.kind, streams: r.createEncodedStreams()}; 116 })); 117 } 118 119 // Munge the SDP to enable the GFD extension in order to get correct metadata. 120 const sdpABS = enableExtension(offer.sdp, ABS_V00_EXTENSION); 121 const sdpGFD = enableExtension(sdpABS, GFD_V00_EXTENSION); 122 // Munge the SDP to disable bandwidth probing via RTX. 123 // TODO(crbug.com/1066819): remove this hack when we do not receive duplicates from RTX 124 // anymore. 125 const sdpRTX = sdpGFD.replace(new RegExp('rtx', 'g'), 'invalid'); 126 await pc1.setRemoteDescription({type: 'offer', sdp: sdpRTX}); 127 await pc2.setLocalDescription({type: 'offer', sdp: sdpGFD}); 128 129 const answer = await pc1.createAnswer(); 130 await pc2.setRemoteDescription(answer); 131 await pc1.setLocalDescription(answer); 132 } 133 134 function createFrameDescriptor(videoFrame) { 135 const kMaxSpatialLayers = 8; 136 const kMaxTemporalLayers = 8; 137 const kMaxNumFrameDependencies = 8; 138 139 const metadata = videoFrame.getMetadata(); 140 let frameDescriptor = { 141 beginningOfSubFrame: true, 142 endOfSubframe: false, 143 frameId: metadata.frameId & 0xFFFF, 144 spatialLayers: 1 << metadata.spatialIndex, 145 temporalLayer: metadata.temporalLayer, 146 frameDependenciesDiffs: [], 147 width: 0, 148 height: 0 149 }; 150 151 for (const dependency of metadata.dependencies) { 152 frameDescriptor.frameDependenciesDiffs.push(metadata.frameId - dependency); 153 } 154 if (metadata.dependencies.length == 0) { 155 frameDescriptor.width = metadata.width; 156 frameDescriptor.height = metadata.height; 157 } 158 return frameDescriptor; 159 } 160 161 function additionalDataSize(descriptor) { 162 if (!descriptor.beginningOfSubFrame) { 163 return 1; 164 } 165 166 let size = 4; 167 for (const fdiff of descriptor.frameDependenciesDiffs) { 168 size += (fdiff >= (1 << 6)) ? 2 : 1; 169 } 170 if (descriptor.beginningOfSubFrame && 171 descriptor.frameDependenciesDiffs.length == 0 && 172 descriptor.width > 0 && 173 descriptor.height > 0) { 174 size += 4; 175 } 176 177 return size; 178 } 179 180 // Compute the buffer reported in the additionalData field using the metadata 181 // provided by a video frame. 182 // Based on the webrtc::RtpDescriptorAuthentication() C++ function at 183 // https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/modules/rtp_rtcp/source/rtp_descriptor_authentication.cc 184 function computeAdditionalData(videoFrame) { 185 const kMaxSpatialLayers = 8; 186 const kMaxTemporalLayers = 8; 187 const kMaxNumFrameDependencies = 8; 188 189 const metadata = videoFrame.getMetadata(); 190 if (metadata.spatialIndex < 0 || 191 metadata.temporalIndex < 0 || 192 metadata.spatialIndex >= kMaxSpatialLayers || 193 metadata.temporalIndex >= kMaxTemporalLayers || 194 metadata.dependencies.length > kMaxNumFrameDependencies) { 195 return new ArrayBuffer(0); 196 } 197 198 const descriptor = createFrameDescriptor(videoFrame); 199 const size = additionalDataSize(descriptor); 200 const additionalData = new ArrayBuffer(size); 201 const data = new Uint8Array(additionalData); 202 203 const kFlagBeginOfSubframe = 0x80; 204 const kFlagEndOfSubframe = 0x40; 205 const kFlagFirstSubframeV00 = 0x20; 206 const kFlagLastSubframeV00 = 0x10; 207 208 const kFlagDependencies = 0x08; 209 const kFlagMoreDependencies = 0x01; 210 const kFlageXtendedOffset = 0x02; 211 212 let baseHeader = 213 (descriptor.beginningOfSubFrame ? kFlagBeginOfSubframe : 0) | 214 (descriptor.endOfSubFrame ? kFlagEndOfSubframe : 0); 215 baseHeader |= kFlagFirstSubframeV00; 216 baseHeader |= kFlagLastSubframeV00; 217 218 if (!descriptor.beginningOfSubFrame) { 219 data[0] = baseHeader; 220 return additionalData; 221 } 222 223 data[0] = 224 baseHeader | 225 (descriptor.frameDependenciesDiffs.length == 0 ? 0 : kFlagDependencies) | 226 descriptor.temporalLayer; 227 data[1] = descriptor.spatialLayers; 228 data[2] = descriptor.frameId & 0xFF; 229 data[3] = descriptor.frameId >> 8; 230 231 const fdiffs = descriptor.frameDependenciesDiffs; 232 let offset = 4; 233 if (descriptor.beginningOfSubFrame && 234 fdiffs.length == 0 && 235 descriptor.width > 0 && 236 descriptor.height > 0) { 237 data[offset++] = (descriptor.width >> 8); 238 data[offset++] = (descriptor.width & 0xFF); 239 data[offset++] = (descriptor.height >> 8); 240 data[offset++] = (descriptor.height & 0xFF); 241 } 242 for (let i = 0; i < fdiffs.length; i++) { 243 const extended = fdiffs[i] >= (1 << 6); 244 const more = i < fdiffs.length - 1; 245 data[offset++] = ((fdiffs[i] & 0x3f) << 2) | 246 (extended ? kFlageXtendedOffset : 0) | 247 (more ? kFlagMoreDependencies : 0); 248 if (extended) { 249 data[offset++] = fdiffs[i] >> 6; 250 } 251 } 252 return additionalData; 253 } 254 255 function verifyNonstandardAdditionalDataIfPresent(videoFrame) { 256 if (videoFrame.additionalData === undefined) 257 return; 258 259 const computedData = computeAdditionalData(videoFrame); 260 assert_true(areArrayBuffersEqual(videoFrame.additionalData, computedData)); 261 }