/* eslint-disable no-new-wrappers */
/* eslint-disable camelcase */
import BitStreamReader from './BitStreamReader'
/**
* Enum of Millicast supported Video codecs
* @readonly
* @enum {String}
* @property {String} VP8
* @property {String} VP9
* @property {String} H264
* @property {String} AV1
* @property {String} H265 - Only available in Safari
*/
export const VideoCodec = {
VP8: 'vp8',
VP9: 'vp9',
H264: 'h264',
AV1: 'av1',
H265: 'h265'
}
/**
* Enum of Millicast supported Audio codecs
* @readonly
* @enum {String}
* @property {String} OPUS
* @property {String} MULTIOPUS
*/
export const AudioCodec = {
OPUS: 'opus',
MULTIOPUS: 'multiopus'
}
const NALUType = {
SLICE_NON_IDR: 1,
SLICE_PARTITION_A: 2,
SLICE_IDR: 5,
SEI_H264: 6,
SEI_H265_PREFIX: 39,
SEI_H265_SUFFIX: 40,
SPS_H264: 7,
SPS_H265: 33,
PPS_H264: 8,
PPS_H265: 34
}
const SEI_Payload_Type = {
PIC_TIMING: 1,
USER_DATA_UNREGISTERED: 5
}
const UNREGISTERED_MESSAGE_TYPE = {
LEGACY: 1,
NEW: 2,
TIMECODE: 3,
OTHER: 4
}
// Old UUID - used by SDK 0.1.46 and before. No longer actively used (only for backwards compatibility purposes)
export const DOLBY_SEI_DATA_UUID = '6e9cfd2a-5907-49ff-b363-8978a6e8340e'
// AMF/KLV timestamps
export const DOLBY_SEI_TIMESTAMP_UUID = '9a21f3be-31f0-4b78-b0be-c7f7dbb97250'
// When the SDK inserts its own timecode into the unregistered block, it uses this identifier
export const DOLBY_SDK_TIMESTAMP_UUID = 'd40e38ea-d419-4c62-94ed-20ac37b4e4fa'
class SPSState {
constructor (codec = 'H264') {
this.sps = new Map()
this.pps = new Map()
this.activeSPS = null
this.codec = codec
}
collectPPS (rbsp) {
if (this.codec === 'H264') {
this.collectH264PPS(rbsp)
} else {
this.collectH265PPS(rbsp)
}
}
collectSPS (rbsp) {
if (this.codec === 'H264') {
this.collectH264SPS(rbsp)
} else {
this.collectH265SPS(rbsp)
}
}
collectH264SPS (rbsp) {
const reader = new BitStreamReader(rbsp)
const profile_idc = reader.readBits(8)
const supported_profiles = [100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135]
reader.skip(8) // skip 8bits constraint set flag and reserved_zero_2bits
reader.skip(8) // level_idc
const seq_parameter_set_id = reader.readExpGolombUnsigned()
if (seq_parameter_set_id > 31 || seq_parameter_set_id < 0) {
throw new Error('Invalid seq_parameter_set_id')
}
if (supported_profiles.includes(profile_idc)) {
const chroma_format_idc = reader.readExpGolombUnsigned()
if (chroma_format_idc === 3) {
reader.skip(1) // separate_colour_plane_flag
}
reader.readExpGolombUnsigned() // bit_depth_luma_minus8
reader.readExpGolombUnsigned() // bit_depth_chroma_minus8
reader.skip(1) // qpprime_y_zero_transform_bypass_flag
const seq_scaling_matrix_present_flag = reader.readBits(1)
if (seq_scaling_matrix_present_flag) {
// parse scaling matrix, since we don't need the result, just read and skip it
const sizeOfScalingList = (chroma_format_idc !== 3) ? 8 : 12
for (let i = 0; i < sizeOfScalingList; i++) {
if (reader.readBits(1)) {
const sizeOfScalingList = (i < 6) ? 16 : 64
let lastScale = 8
let nextScale = 8
for (let j = 0; j < sizeOfScalingList; j++) {
if (nextScale !== 0) {
const delta_scale = reader.readExpGolombSigned()
nextScale = (lastScale + delta_scale + 256) % 256
}
lastScale = nextScale === 0 ? lastScale : nextScale
}
}
}
}
}
reader.readExpGolombUnsigned() // log2_max_frame_num_minus4
const pic_order_cnt_type = reader.readExpGolombUnsigned()
if (pic_order_cnt_type === 0) {
reader.readExpGolombUnsigned() // log2_max_pic_order_cnt_lsb_minus4
} else if (pic_order_cnt_type === 1) {
reader.skip(1) // delta_pic_order_always_zero_flag
reader.readExpGolombSigned() // offset_for_non_ref_pic
reader.readExpGolombSigned() // offset_for_top_to_bottom_field
const num_ref_frames_in_pic_order_cnt_cycle = reader.readExpGolombUnsigned()
for (let i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++) {
// parse offset_for_ref_frame
reader.readExpGolombSigned()
}
}
reader.readExpGolombUnsigned() // max_num_ref_frames
reader.skip(1) // gaps_in_frame_num_value_allowed_flag
reader.readExpGolombUnsigned() // pic_width_in_mbs_minus1
reader.readExpGolombUnsigned() // pic_height_in_map_units_minus1
if (reader.readBits(1) === 0) reader.skip(1) // frame_mbs_only_flag and mb_adaptive_frame_field_flag
reader.skip(1) // direct_8x8_inference_flag
// parse frame_crop
if (reader.readBits(1)) {
reader.readExpGolombUnsigned() // frame_crop_left_offset
reader.readExpGolombUnsigned() // frame_crop_right_offset
reader.readExpGolombUnsigned() // frame_crop_top_offset
reader.readExpGolombUnsigned() // frame_crop_bottom_offset
}
// parse vui_parameters
let vui_parameters
if (reader.readBits(1)) {
// aspect_ratio_info
if (reader.readBits(1)) {
const aspect_ratio_idc = reader.readBits(8)
if (aspect_ratio_idc === 255) {
// Extended_SAR
reader.skip(16)
reader.skip(16)
}
}
// overscan_info
if (reader.readBits(1)) {
reader.skip(1)
}
// video_signal_type
if (reader.readBits(1)) {
reader.skip(3) // video_format
reader.skip(1) // video_full_range_flag
if (reader.readBits(1)) {
// colour_description
reader.skip(24)
}
}
// chroma_loc_info
if (reader.readBits(1)) {
reader.readExpGolombUnsigned() // chroma_sample_loc_type_top_field
reader.readExpGolombUnsigned() // chroma_sample_loc_type_bottom_field
}
// timing_info
const timing_info = reader.readBits(1)
? {
num_units_in_tick: reader.readBits(32),
time_scale: reader.readBits(32),
fixed_frame_rate_flag: reader.readBits(1)
}
: undefined
function parseHRDParameters (reader) {
const cpb_cnt_minus1 = reader.readExpGolombUnsigned()
reader.skip(4) // bit_rate_scale
reader.skip(4) // cpb_size_scale
for (let i = 0; i <= cpb_cnt_minus1; i++) {
reader.readExpGolombUnsigned() // bit_rate_value_minus1
reader.readExpGolombUnsigned() // cpb_size_value_minus1
reader.skip(1) // cbr_flag
}
reader.skip(5) // initial_cpb_removal_delay_length_minus1
const cpb_removal_delay_length_minus1 = reader.readBits(5)
const dpb_output_delay_length_minus1 = reader.readBits(5)
const time_offset_length = reader.readBits(5)
return {
cpb_removal_delay_length_minus1,
dpb_output_delay_length_minus1,
time_offset_length
}
}
const nal_hrd_parameters = reader.readBits(1) ? parseHRDParameters(reader) : undefined
const vcl_hrd_parameters = reader.readBits(1) ? parseHRDParameters(reader) : undefined
if (nal_hrd_parameters || vcl_hrd_parameters) {
reader.skip(1) // low_delay_hrd_flag
}
const pic_struct_present_flag = reader.readBits(1)
vui_parameters = {
timing_info,
nal_hrd_parameters,
vcl_hrd_parameters,
pic_struct_present_flag
}
}
this.sps.set(seq_parameter_set_id, {
vui_parameters
})
}
collectH265SPS (rbsp) {
// TODO: parse H265 SPS
}
collectH264PPS (rbsp) {
const reader = new BitStreamReader(rbsp)
const pic_parameter_set_id = reader.readExpGolombUnsigned()
if (pic_parameter_set_id > 255 || pic_parameter_set_id < 0) {
throw new Error('Invalid pic_parameter_set_id')
}
const seq_parameter_set_id = reader.readExpGolombUnsigned()
this.pps.set(pic_parameter_set_id, {
seq_parameter_set_id
})
}
collectH265PPS (rbsp) {
// TODO: parse H265 PPS
}
findActiveSPS (rbsp) {
// get the seq_parameter_set_id from the slice header
const reader = new BitStreamReader(rbsp)
reader.readExpGolombUnsigned() // first_mb_in_slice
reader.readExpGolombUnsigned() // slice_type
const pic_parameter_set_id = reader.readExpGolombUnsigned()
const pps = this.pps.get(pic_parameter_set_id)
if (pps) {
const sps = this.sps.get(pps.seq_parameter_set_id)
if (sps) {
this.activeSPS = sps
return
}
}
throw new Error('Cannot find the active SPS')
}
}
const spsState = new SPSState()
function findStartCodeIndex (frameBuffer, offset) {
while (offset < frameBuffer.byteLength - 4) {
if ((frameBuffer[offset] === 0x00 && frameBuffer[offset + 1] === 0x00) &&
(frameBuffer[offset + 2] === 0x01 ||
(frameBuffer[offset + 2] === 0x00 && frameBuffer[offset + 3] === 0x01))) {
return offset
} else {
offset += 1
}
}
return -1
}
// EBSP (Encapsulate Byte Sequence Payload) is a sequence of bytes without start code and header in NAL unit
// which is also aka ...RBSP (Raw Byte Sequence Payload) + 0x03 when there are [0x00, 0x00, <0x01 or 0x02 or 0x03>] in RBSP
// so we need to remove 0x03 byte before we parse the payload data
function removePreventionBytes (ebsp) {
const output = new Uint8Array(ebsp.byteLength)
let outOffset = 0
let ebspOffset = 0
for (let preventionByteIdx = 2; preventionByteIdx < ebsp.byteLength; preventionByteIdx++) {
if (ebsp[preventionByteIdx] === 0x03 && ebsp[preventionByteIdx - 1] === 0x00 && ebsp[preventionByteIdx - 2] === 0x00) {
output.set(ebsp.subarray(ebspOffset, preventionByteIdx), outOffset)
outOffset += preventionByteIdx - ebspOffset
ebspOffset = preventionByteIdx + 1
}
}
if (ebspOffset < ebsp.byteLength) {
output.set(ebsp.subarray(ebspOffset), outOffset)
}
return output
}
function getNalus (frameBuffer, codec) {
let offset = 0
const headerSize = codec === 'H264' ? 1 : 2
const nalus = []
while (offset < frameBuffer.byteLength - 4) {
const startCodeIndex = findStartCodeIndex(frameBuffer, offset)
if (startCodeIndex >= offset) {
// found the NAL unit start code
const startCodeLength = frameBuffer[startCodeIndex + 2] === 0x01 ? 3 : 4
// find the start index of next NAL unit
const nextStartCodeIndex = findStartCodeIndex(frameBuffer, startCodeIndex + startCodeLength + headerSize)
if (nextStartCodeIndex > startCodeIndex) {
nalus.push(frameBuffer.subarray(startCodeIndex, nextStartCodeIndex))
offset = nextStartCodeIndex
} else {
nalus.push(frameBuffer.subarray(startCodeIndex))
break
}
} else {
break
}
}
return nalus
}
function getSeiNalus (frameBuffer, codec) {
let shouldSearchActiveSPS = true
return getNalus(frameBuffer, codec).filter((nalu) => {
const startCodeLength = nalu[2] === 0x01 ? 3 : 4
const headerLength = codec === 'H264' ? 1 : 2
const header = nalu[startCodeLength]
const naluType = codec === 'H264' ? header & 0x1f : header >> 1 & 0x3f
if (shouldSearchActiveSPS) {
switch (naluType) {
case NALUType.PPS_H264:
case NALUType.PPS_H265:
spsState.collectPPS(removePreventionBytes(nalu.subarray(startCodeLength + headerLength)))
break
case NALUType.SPS_H264:
case NALUType.SPS_H265:
spsState.collectSPS(removePreventionBytes(nalu.subarray(startCodeLength + headerLength)))
break
case NALUType.SLICE_IDR:
case NALUType.SLICE_NON_IDR:
case NALUType.SLICE_PARTITION_A:
try {
spsState.findActiveSPS(removePreventionBytes(nalu.subarray(startCodeLength + headerLength)))
shouldSearchActiveSPS = false
} catch (err) {
console.info('Failed to find active SPS. Will not be able to extract PIC timing metadata')
}
break
default:
break
}
}
return [NALUType.SEI_H264, NALUType.SEI_H265_PREFIX, NALUType.SEI_H265_SUFFIX].includes(naluType)
})
}
function extractSEIPayload (rbsp) {
let payloadType = 0
let idx = 0
while (rbsp[idx] === 0xff) {
payloadType += 0xff
idx++
}
payloadType += rbsp[idx]
idx++
let payloadSize = 0
while (rbsp[idx] === 0xff) {
payloadSize += 0xff
idx++
}
payloadSize += rbsp[idx]
idx++
return {
type: payloadType,
content: rbsp.subarray(idx, idx + payloadSize)
}
}
function resolveUnregisteredMessageType (uuid) {
const timecodeUuid = new Uint8Array(parseUUID(DOLBY_SEI_TIMESTAMP_UUID))
const legacySdkUuid = new Uint8Array(parseUUID(DOLBY_SEI_DATA_UUID))
const newSdkUuid = new Uint8Array(parseUUID(DOLBY_SDK_TIMESTAMP_UUID))
if (timecodeUuid.every((value, index) => value === uuid[index])) return UNREGISTERED_MESSAGE_TYPE.TIMECODE
if (legacySdkUuid.every((value, index) => value === uuid[index])) return UNREGISTERED_MESSAGE_TYPE.LEGACY
if (newSdkUuid.every((value, index) => value === uuid[index])) return UNREGISTERED_MESSAGE_TYPE.NEW
return UNREGISTERED_MESSAGE_TYPE.OTHER
}
function getSeiUserUnregisteredData (metadata, payloadContent) {
let idx = 0
metadata.uuid = payloadContent.subarray(idx, idx + 16)
idx += 16
// There are 3 possible pathways here
// the old UUID - where-in there is no timecode in the body
// the new UUID - where-in there is timecode in the body
// AMF/KLV timestamps - where there is no other data to parse
const messageType = resolveUnregisteredMessageType(metadata.uuid)
const content = payloadContent.subarray(idx)
switch (messageType) {
case UNREGISTERED_MESSAGE_TYPE.LEGACY:
case UNREGISTERED_MESSAGE_TYPE.OTHER:
metadata.unregistered = content
break
case UNREGISTERED_MESSAGE_TYPE.TIMECODE:
metadata.timecode = convertSEITimestamp(content)
break
case UNREGISTERED_MESSAGE_TYPE.NEW: {
// we need to separate two sub-arrays here - one for timecode and the remainer to unregistered
// Do not make assumptions on the size of a date object as a byte array
let index = 0
const timecodeBufferLength = numberToByteArray(Date.now()).length
const timecodeSubArray = content.subarray(index, timecodeBufferLength)
index += timecodeBufferLength
const metadataSubArray = content.subarray(index)
metadata.timecode = convertSEITimestamp(timecodeSubArray)
metadata.unregistered = metadataSubArray
break
}
}
}
function convertSEITimestamp (data) {
const timestampBigInt = data.reduce((acc, byte) => (acc << BigInt(8)) + BigInt(byte), BigInt(0))
const milliseconds = Number(timestampBigInt)
const date = new Date(milliseconds)
const dateEncoded = new TextEncoder().encode(date.toISOString())
return dateEncoded
}
function getSeiPicTimingTimecode (metadata, payloadContent) {
if (!spsState.activeSPS) {
console.warn('Cannot find the active SPS')
return
}
const hrdParameters = spsState.activeSPS.vui_parameters.nal_hrd_parameters ?? spsState.activeSPS.vui_parameters.vcl_hrd_parameters
const options = {
cpb_dpb_delays_present_flag: hrdParameters ? 1 : 0,
cpb_removal_delay_length_minus1: hrdParameters?.cpb_removal_delay_length_minus1 ?? 23,
dpb_output_delay_length_minus1: hrdParameters?.dpb_output_delay_length_minus1 ?? 23,
time_offset_length: hrdParameters ? hrdParameters.time_offset_length ?? 24 : undefined,
pic_struct_present_flag: spsState.activeSPS.vui_parameters.pic_struct_present_flag ?? 0
}
if (!options.pic_struct_present_flag) {
console.warn('pic_struct_present_flag is not present')
return undefined
}
const reader = new BitStreamReader(payloadContent)
if (options.cpb_dpb_delays_present_flag) {
reader.skip(options.cpb_removal_delay_length_minus1 + 1) // cpb_removal_delay
reader.skip(options.dpb_output_delay_length_minus1 + 1) // dpb_output_delay
}
const picStructNumClockTS = [1, 1, 1, 2, 2, 3, 3, 2, 3]
const pic_struct = reader.readBits(4)
if (pic_struct >= picStructNumClockTS.length) {
throw new Error('Invalid pic_struct')
}
const numClockTS = picStructNumClockTS[pic_struct]
const timecodes = []
for (let i = 0; i < numClockTS; i++) {
const clock_timestamp_flag = reader.readBits(1)
if (clock_timestamp_flag) {
const timecode = {}
reader.skip(2) // ct_type
reader.skip(1) // nuit_field_based_flag
reader.skip(5) // counting_type
const full_timestamp_flag = reader.readBits(1)
reader.skip(2) // discontinuity_flag, cnt_dropped_flag
timecode.n_frames = reader.readBits(8)
if (full_timestamp_flag) {
timecode.seconds_value = reader.readBits(6)
timecode.minutes_value = reader.readBits(6)
timecode.hours_value = reader.readBits(5)
} else {
const seconds_flag = reader.readBits(1)
if (seconds_flag) {
timecode.seconds_value = reader.readBits(6)
const minutes_flag = reader.readBits(1)
if (minutes_flag) {
timecode.minutes_value = reader.readBits(6)
const hours_flag = reader.readBits(1)
if (hours_flag) {
timecode.hours_value = reader.readBits(5)
}
}
}
}
if (options.time_offset_length) {
try {
timecode.time_offset = reader.readBits(options.time_offset_length)
} catch (err) {
console.error('Failed to read time_offset', err)
timecode.time_offset = 0
}
} else {
timecode.time_offset = 0
}
timecodes.push(timecode)
}
}
metadata.seiPicTimingTimeCodeArray = timecodes
}
/**
* SEI User unregistered data
* @typedef {object} SEIUserUnregisteredData
* @global
* @property {string} uuid - the UUID of the SEI user unregistered data
* @property {Uint8Array} data - the binary content of the SEI user unregistered data
*/
/**
* SEI Pic timing time code
* @typedef {object} SEIPicTimingTimeCode
* @global
* @property {number} seconds
* @property {number} minutes
* @property {number} hours
* @property {number} n_frames
* @property {number} time_offset
*/
/**
* Metadata of the Encoded Frame
* @typedef {object} FrameMetaData
* @global
* @property {number} timestamp - the time at which frame sampling started, value is a positive integer containing the sampling instant of the first byte in this frame, in microseconds
* @property { Array<SEIUserUnregisteredData> } seiUserUnregisteredDataArray - the SEI user unregistered data array
* @property { Array<SEIPicTimingTimeCode> } [seiPicTimingTimeCodeArray] - the SEI pic timing time codes
*/
/**
* Extract user unregistered metadata from H26x Encoded Frame
* @param { RTCEncodedFrame } encodedFrame
* @param { 'H264' | 'H265' } codec
* @returns { FrameMetaData }
*/
export function extractH26xMetadata (encodedFrame, codec) {
if (codec !== 'H264' && codec !== 'H265') {
throw new Error(`Unsupported codec ${codec}`)
}
const metadata = {}
spsState.codec = codec
getSeiNalus(new Uint8Array(encodedFrame.data), codec).forEach((nalu) => {
const startCodeLength = nalu[2] === 0x01 ? 3 : 4
const headerLength = codec === 'H264' ? 1 : 2
const rbsp = removePreventionBytes(nalu.subarray(startCodeLength + headerLength))
const payload = extractSEIPayload(rbsp)
switch (payload.type) {
case SEI_Payload_Type.PIC_TIMING:
getSeiPicTimingTimecode(metadata, payload.content)
break
case SEI_Payload_Type.USER_DATA_UNREGISTERED:
getSeiUserUnregisteredData(metadata, payload.content)
break
default:
break
}
})
return metadata
}
function isValidUUID (uuid) {
const uuidRegEx = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/
return uuidRegEx.test(uuid)
}
function parseUUID (uuid) {
return uuid.replace(/-/g, '')
.match(/.{1,2}/g)
.map(byte => parseInt(byte, 16))
}
function createSEIMessageContent (uuid, payload, timecode) {
const uuidArray = new Uint8Array(parseUUID(uuid))
const timecodeArray = numberToByteArray(timecode)
const payloadArray = new TextEncoder().encode(JSON.stringify(payload))
const content = new Uint8Array(uuidArray.length + timecodeArray.length + payloadArray.length)
content.set(uuidArray)
content.set(timecodeArray, uuidArray.length)
content.set(payloadArray, (timecodeArray.length + uuidArray.length))
return content
}
function createSEITypeAndSize (content) {
const payloadSize = []
const ffBytes = Math.floor(content.byteLength / 255)
const lastPayloadTypeByte = content.byteLength % 255
for (let i = 0; i < ffBytes; i++) {
payloadSize.push(0xFF)
}
payloadSize.push(lastPayloadTypeByte)
return new Uint8Array([0x05, ...payloadSize])
}
function createSEIMessageContentWithPrevensionBytes (content) {
const preventionByteArray = []
for (let i = 0; i < content.byteLength; i++) {
if (i + 2 < content.byteLength && [0x00, 0x01, 0x02, 0x03].includes(content[i + 2]) && content[i] === 0x00 && content[i + 1] === 0x00) {
preventionByteArray.push(content[i])
preventionByteArray.push(content[i + 1])
i += 2
preventionByteArray.push(0x03)
} else {
preventionByteArray.push(content[i])
}
}
// trailing bits
preventionByteArray.push(0x80)
return new Uint8Array(preventionByteArray)
}
function numberToByteArray (num) {
const array = []
if (!isNaN(num)) {
const bigint = BigInt(num)
for (let i = 0; i < Math.ceil(Math.floor(Math.log2(new Number(num)) + 1) / 8); i++) {
array.unshift(new Number((bigint >> BigInt(8 * i)) & BigInt(255)))
}
}
return new Uint8Array(array)
}
function createSEINalu ({ uuid, payload, timecode }) {
const startCode = [0x00, 0x00, 0x00, 0x01]
const header = [0x66] // 0b01100110
const content = createSEIMessageContent(uuid, payload, timecode)
const seiTypeAndSize = createSEITypeAndSize(content)
const contentWithPreventionBytes = createSEIMessageContentWithPrevensionBytes(content)
const naluWithSEI = new Uint8Array(startCode.length + header.length + seiTypeAndSize.length + contentWithPreventionBytes.length)
naluWithSEI.set(startCode)
naluWithSEI.set(header, startCode.length)
naluWithSEI.set(seiTypeAndSize, startCode.length + header.length)
naluWithSEI.set(contentWithPreventionBytes, startCode.length + header.length + seiTypeAndSize.length)
return naluWithSEI
}
export function addH26xSEI ({ uuid, payload, timecode }, encodedFrame) {
if (uuid === '' || payload === '') {
throw new Error('uuid and payload cannot be empty')
}
if (!isValidUUID(uuid)) {
console.warn('Invalid UUID. Using default UUID.')
uuid = DOLBY_SDK_TIMESTAMP_UUID
timecode = Date.now()
}
// Case of NALU H264 - User Unregistered Data
const naluWithSEI = createSEINalu({ uuid, payload, timecode })
const encodedFrameView = new DataView(encodedFrame.data)
const encodedFrameWithSEI = new ArrayBuffer(encodedFrame.data.byteLength + naluWithSEI.byteLength)
const encodedFrameWithSEIView = new DataView(encodedFrameWithSEI)
for (let i = 0; i < encodedFrame.data.byteLength; i++) {
encodedFrameWithSEIView.setUint8(i, encodedFrameView.getUint8(i))
}
for (let i = 0; i < naluWithSEI.byteLength; i++) {
encodedFrameWithSEIView.setUint8(encodedFrame.data.byteLength + i, naluWithSEI[i])
}
encodedFrame.data = encodedFrameWithSEI
}