Separate MKV class and functions in a separate module.

This commit is contained in:
Frédéric Tronel
2026-08-28 22:57:50 +02:00
parent ff20f31e74
commit 92df1d24d6
5 changed files with 502 additions and 471 deletions
+332
View File
@@ -0,0 +1,332 @@
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Copyright (C) 2026 Frédéric Tronel
import logging
from dataclasses import dataclass, field
from math import floor
from typing import IO
from typeguard import typechecked
import hexdump
from tscut.h264.bitstream import (
read_bit,
read_bits,
read_byte,
read_long,
read_word,
write_bits,
write_byte,
write_word,
sodb_to_rbsp,
rbsp_to_sodb
)
from tscut.h264.parameters import (
SPS,
PPS,
)
from tscut.matroska.ebml import get_ebml_length
logger = logging.getLogger(__name__)
@dataclass
class AVCDecoderConfiguration:
configuration_version:int=1 # u(8)
avc_profile_indication:int=0 # u(8)
profile_compatibility:int=0 # u(8)
avc_level_indication:int=0 # u(8)
length_size_minus_one:int=0 # u(2) (0,1 or 3)
num_of_sequence_parameter_sets:int=0 # u(5)
sps:dict = field(default_factory=dict)
num_of_picture_parameter_sets:int=0 #u(8)
pps:dict = field(default_factory=dict)
chroma_format:int=0 # u(2)
bit_depth_luma_minus8:int=0 # u(3)
bit_depth_chroma_minus8:int=0 # u(3)
num_of_sequence_parameter_set_ext:int=0 # u(8)
spsext:dict = field(default_factory=dict)
def __init__(self):
self.sps = {}
self.spsext = {}
self.pps = {}
def from_bytes(self, buf):
logger.debug('Parsing: %s', (hexdump.dump(buf,sep=':')))
bit_position = 0
bit_position, self.configuration_version = read_byte(buf, bit_position)
bit_position, self.avc_profile_indication = read_byte(buf, bit_position)
bit_position, self.profile_compatibility = read_byte(buf, bit_position)
bit_position, self.avc_level_indication = read_byte(buf, bit_position)
bit_position, v = read_bits(buf, bit_position, 6)
if v != 0b111111:
raise ValueError(f'Reserved bits are not equal to 0b111111: {v:x}')
bit_position, self.length_size_minus_one = read_bits(buf, bit_position, 2)
bit_position, v = read_bits(buf, bit_position, 3)
if v != 0b111:
raise ValueError(f'Reserved bits are not equal to 0b111: {v:x}')
bit_position, self.num_of_sequence_parameter_sets= read_bits(buf, bit_position, 5)
logger.debug('Number of SPS: %d', self.num_of_sequence_parameter_sets)
for _ in range(self.num_of_sequence_parameter_sets):
bit_position, length = read_word(buf, bit_position)
if bit_position % 8 != 0:
raise ValueError(f'SPS is not located at a byte boundary: {bit_position:d}')
sps = SPS()
sodb = rbsp_to_sodb(buf[floor(bit_position/8):])
bit_length = sps.from_bytes(sodb)
spsid = sps.seq_parameter_set_id
self.sps[spsid] = sps
parsed_length = floor(bit_length/8)
logger.debug('Expected length of SPS: %d bytes. Parsed: %d bytes', length,
parsed_length)
# Parse length can be shorter than length because of rewriting from RBSP to SODB
# (that is shorter).
# So we advance of indicated length.
bit_position+=length*8
logger.debug('Bit position:%d. Reading one byte of: %s', bit_position,
hexdump.dump(buf[floor(bit_position/8):], sep=':'))
bit_position, self.num_of_picture_parameter_sets = read_byte(buf, bit_position)
logger.debug('Number of PPS: %d', self.num_of_picture_parameter_sets)
for _ in range(self.num_of_picture_parameter_sets):
bit_position, length = read_word(buf, bit_position)
if bit_position % 8 != 0:
raise ValueError('PPS is not located at a byte boundary: {bit_position:d}')
pps = PPS()
sodb = rbsp_to_sodb(buf[floor(bit_position/8):])
bit_length = pps.from_bytes(sodb, self.chroma_format)
ppsid = pps.pic_parameter_set_id
self.pps[ppsid] = pps
parsed_length = floor(bit_length/8)
logger.debug('Expected length of PPS: %d bytes. Parsed: %d bytes', length,
parsed_length)
# Parse length can be shorter than length because of rewriting from RBSP to SODB
# (that is shorter).
# So we advance of indicated length.
bit_position+=length*8
logger.debug('Remaining bits: %s', hexdump.dump(buf[floor(bit_position/8):]))
if self.avc_profile_indication in [100, 110, 122, 144]:
bit_position, reserved = read_bits(buf, bit_position, 6)
if reserved != 0b111111:
raise ValueError(f'Reserved bits are different from 111111: {reserved:x}')
bit_position, self.chroma_format = read_bits(buf, bit_position, 2)
bit_position, reserved = read_bits(buf, bit_position, 5)
if reserved != 0b11111:
raise ValueError(f'Reserved bits are different from 11111: {reserved:x}')
bit_position, self.bit_depth_luma_minus8 = read_bits(buf, bit_position, 3)
bit_position, reserved = read_bits(buf, bit_position, 5)
if reserved != 0b11111:
raise ValueError(f'Reserved bits are different from 11111: {reserved:x}')
bit_position, self.bit_depth_chroma_minus8 = read_bits(buf, bit_position, 3)
bit_position, self.num_of_sequence_parameter_set_ext = read_byte(buf, bit_position)
for _ in range(self.num_of_sequence_parameter_set_ext):
# TODO: parse SPSextended
logger.error('Parsing of SPS extended not yet implemented !')
pass
def to_bytes(self):
buf = bytearray()
bit_position = 0
bit_position = write_byte(buf, bit_position, self.configuration_version)
bit_position = write_byte(buf, bit_position, self.avc_profile_indication)
bit_position = write_byte(buf, bit_position, self.profile_compatibility)
bit_position = write_byte(buf, bit_position, self.avc_level_indication)
bit_position = write_bits(buf, bit_position, 0b111111, 6)
bit_position = write_bits(buf, bit_position, self.length_size_minus_one, 2)
bit_position = write_bits(buf, bit_position, 0b111, 3)
bit_position = write_bits(buf, bit_position, self.num_of_sequence_parameter_sets, 5)
for spsid, sps in self.sps.items():
sodb = sps.to_bytes()
sodb_length = len(sodb)
rbsp = sodb_to_rbsp(sodb)
rbsp_length = len(rbsp)
logger.debug('SODB length: %d RBSP length:%d', sodb_length, rbsp_length)
bit_position = write_word(buf, bit_position, rbsp_length)
buf.extend(rbsp)
bit_position+=rbsp_length*8
logger.debug('2. Buffer: %s', hexdump.dump(buf, sep=':'))
bit_position = write_byte(buf, bit_position, self.num_of_picture_parameter_sets)
for ppsid, lpps in self.pps.items():
logger.debug('Writing PPS: %d', ppsid)
# TODO: does chroma_format should come from self ?
sodb = lpps.to_bytes(self.chroma_format)
sodb_length = len(sodb)
rbsp = sodb_to_rbsp(sodb)
rbsp_length = len(rbsp)
logger.debug('SODB length: %d RBSP length:%d', sodb_length, rbsp_length)
bit_position = write_word(buf, bit_position, rbsp_length)
buf.extend(rbsp)
bit_position+=rbsp_length*8
if self.avc_profile_indication in [ 100, 110, 122, 144]:
bit_position = write_bits(buf, bit_position, 0b111111, 6)
bit_position = write_bits(buf, bit_position, self.chroma_format, 2)
bit_position = write_bits(buf, bit_position, 0b11111, 5)
bit_position = write_bits(buf, bit_position, self.bit_depth_luma_minus8, 3)
bit_position = write_bits(buf, bit_position, 0b11111, 5)
bit_position = write_bits(buf, bit_position, self.bit_depth_chroma_minus8, 3)
bit_position = write_byte(buf, bit_position, self.num_of_sequence_parameter_set_ext)
for _ in range(self.num_of_sequence_parameter_set_ext):
# TODO: dump SPSextended
logger.error('Dumping SPS extended not yet implemented')
pass
return buf
def merge(self, config):
# Check config compatibility
if self.configuration_version != config.configuration_version:
raise ValueError(f'Configuration versions are different: {self.configuration_version:d}\
vs {config.configuration_version:d}')
if self.avc_profile_indication != config.avc_profile_indication:
raise ValueError(f'AVC profiles are different: {self.avc_profile_indication:d} vs \
{config.avc_profile_indication:d}')
if self.profile_compatibility != config.profile_compatibility:
raise ValueError(f'Profile compatilities are different: {self.profile_compatibility:d} \
vs {config.profile_compatibility:d}')
if self.avc_level_indication != config.avc_level_indication:
raise ValueError(f'Level indications are different: {self.avc_level_indication:d} vs \
{config.avc_level_indication:d}')
if self.length_size_minus_one != config.length_size_minus_one:
raise ValueError(f'Length units are different: {self.length_size_minus_one:d} vs \
{config.length_size_minus_one:d}')
if self.chroma_format != config.chroma_format:
raise ValueError(f'Colour format are different: {self.chroma_format:d} vs \
{config.chroma_format:d}')
if self.bit_depth_luma_minus8 != config.bit_depth_luma_minus8:
raise ValueError(f'Depth of luminance are different: {self.bit_depth_luma_minus8:d} vs \
{config.bit_depth_luma_minus8:d}')
if self.bit_depth_chroma_minus8 != config.bit_depth_chroma_minus8:
raise ValueError(f'Depth of chromaticity are different: \
{self.bit_depth_chroma_minus8:d} vs {config.bit_depth_luma_minus8:d}')
for spsid in config.sps:
sps = config.sps[spsid]
if spsid in self.sps:
localsps = self.sps[spsid]
if sps!=localsps:
raise ValueError(f'Profile are not compatible. They contain two different SPS\
with the same identifier ({spsid:d}): {localsps}\n{sps}\n')
self.sps[spsid] = sps
self.num_of_sequence_parameter_sets = len(self.sps)
for ppsid in config.pps:
pps = config.pps[ppsid]
if ppsid in self.pps:
localpps = self.pps[ppsid]
if pps!=localpps:
raise ValueError(f'Profile are not compatible. They contain two different PPS\
with the same identifier ({ppsid:d}): {localpps}\n{pps}\n')
self.pps[ppsid] = pps
self.num_of_picture_parameter_sets = len(self.pps)
# TODO: do the same with extended SPS !
@typechecked
def parse_codec_private(codec_private_data: bytes) -> AVCDecoderConfiguration:
if codec_private_data[0] != 0x63:
raise ValueError(f'Matroska header is wrong: {codec_private_data[0]:x}')
if codec_private_data[1] != 0xA2:
raise ValueError(f'Matroska header is wrong: {codec_private_data[1]:x}')
length = codec_private_data[2]
if length == 0:
raise ValueError('Matroska length cannot start with zero byte.')
for nb_zeroes in range(8):
b = read_bit(codec_private_data[2:], nb_zeroes)
if b != 0:
break
mask = 2^(7-nb_zeroes)-1
length = codec_private_data[2] and mask
for i in range(nb_zeroes):
length*=256
length+=(codec_private_data[3+i])
byte_position = 3+nb_zeroes
avcconfig = AVCDecoderConfiguration()
avcconfig.from_bytes(codec_private_data[byte_position:])
return avcconfig
@typechecked
def get_avc_config_from_h264(input_file: IO[bytes]) -> AVCDecoderConfiguration:
# TODO: improve this ...
rbsp = input_file.read(1000)
sodb = rbsp_to_sodb(rbsp)
bit_position = 0
bit_position, start_code = read_long(sodb, bit_position)
if start_code != 1:
raise ValueError(f'Starting code not detected: {start_code:x}')
sps = SPS()
bit_length = sps.from_bytes(sodb[4:])
bit_position+=bit_length
bit_position, start_code = read_long(sodb, bit_position)
if start_code != 1:
raise ValueError(f'Starting code not detected: {start_code:x}')
pps = PPS()
bit_length = pps.from_bytes(sodb[floor(bit_position/8):], sps.chroma_format_idc)
logger.debug(pps)
avcconfig = AVCDecoderConfiguration()
avcconfig.configuration_version = 1
avcconfig.avc_profile_indication = sps.profile_idc
avcconfig.profile_compatibility = 0
avcconfig.avc_level_indication = sps.level_idc
avcconfig.length_size_minus_one = 3
avcconfig.num_of_sequence_parameter_sets = 1
avcconfig.num_of_picture_parameter_sets = 1
avcconfig.num_of_sequence_parameter_set_ext = 0
avcconfig.chroma_format = sps.chroma_format_idc
avcconfig.bit_depth_chroma_minus8 = sps.bit_depth_chroma_minus8
avcconfig.bit_depth_luma_minus8 = sps.bit_depth_luma_minus8
avcconfig.sps[sps.seq_parameter_set_id] = sps
avcconfig.pps[pps.pic_parameter_set_id] = pps
return avcconfig
# Unused ?
@typechecked
def get_codec_private_data_from_h264(input_file: IO[bytes]) -> AVCDecoderConfiguration:
avcconfig = get_avc_config_from_h264(input_file)
res = dump_codec_private_data(avcconfig)
return res
@typechecked
def dump_codec_private_data(avc_decoder_configuration: AVCDecoderConfiguration) -> bytearray:
# Rebuild a Matroska Codec Private Element
res = bytearray()
# Code private element
res.extend(b'\x63\xA2')
buf = avc_decoder_configuration.to_bytes()
logger.debug('AVC configuration bitstream: %s (length: %d))', hexdump.dump(buf, sep=':'),
len(buf))
embl_length = get_ebml_length(len(buf))
logger.debug('EMBL encoded length: %s', hexdump.dump(embl_length, sep=':'))
res.extend(embl_length)
res.extend(buf)
return res
+4
View File
@@ -1,3 +1,7 @@
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Copyright (C) 2026 Frédéric Tronel
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from math import floor, log, ceil from math import floor, log, ceil
View File
+163
View File
@@ -0,0 +1,163 @@
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Copyright (C) 2026 Frédéric Tronel
import logging
from os import (
SEEK_SET,
fstat,
ftruncate,
lseek,
read,
write,
)
from typing import IO
from typeguard import typechecked
import hexdump
logger = logging.getLogger(__name__)
#MKV is formatted as an EBML file (Extended Binary Markup Langage).
# cf http://matroska-org.github.io/libebml/specs.html
# It is a Type, Length, Value (TLV) kind of binary file.
# Types are encoded as follows:
# 1xxx xxxx - Class A IDs (2^7 -1 possible values)
# 01xx xxxx xxxx xxxx - Class B IDs (2^14-1 possible values)
# 001x xxxx xxxx xxxx xxxx xxxx - Class C IDs (2^21-1 possible values)
# 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx - Class D IDs (2^28-1 possible values)
# Lengths are encoded as follows:
# 1xxx xxxx
# value 0 to 2^7-2
# 01xx xxxx xxxx xxxx
# value 0 to 2^14-2
# 001x xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^21-2
# 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^28-2
# 0000 1xxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^35-2
# 0000 01xx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^42-2
# 0000 001x xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^49-2
# 0000 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^56-2
@typechecked
def get_ebml_length(length:int) -> bytes|None:
if 0 <= length <= 2**7-2:
size = 1
elif length <= 2**14-2:
size = 2
elif length <= 2**21-2:
size = 3
elif length <= 2**28-2:
size = 4
elif length <= 2**35-2:
size = 5
elif length <= 2**42-2:
size = 6
elif length <= 2**49-2:
size = 7
elif length <= 2**56-2:
size = 8
elif length < 0:
logger.error('Impossible to encode a negative length with EBML.')
return None
else:
logger.error('Impossible to encode a length larger than 2^56-2 with EBML.')
return None
encoded_length = length + ((128>>(size-1))<<((size-1)*8))
res = (encoded_length).to_bytes(size, byteorder='big')
return res
@typechecked
def change_ebml_element_size(input_file: IO[bytes], position:int, addendum:int) -> int:
initial_position = position
infd = input_file.fileno()
lseek(infd, position, SEEK_SET)
buf = read(infd, 1)
element_type = int.from_bytes(buf, byteorder='big')
mask=128
found = False
for i in range(1,5):
if element_type&mask:
type_size = i
found = True
break
mask = mask>>1
if not found:
logger.error('Size of element type cannot be determined: %d', element_type)
raise ValueError()
# We seek to size
position+=type_size
lseek(infd, position, SEEK_SET)
buf = read(infd, 1)
size_head = int.from_bytes(buf, byteorder='big')
logger.info('First byte of size: %x', size_head)
mask=128
found = False
for i in range(1,9):
if size_head&mask:
size_of_data_size = i
found = True
break
mask = mask>>1
if not found:
logger.error('Size of data size cannot be determined: %d', size_head)
raise ValueError()
logger.info('Size of data size: %d.', size_of_data_size)
lseek(infd, position, SEEK_SET)
old_size_buf = read(infd, size_of_data_size)
max_size = 2**(size_of_data_size*7)-2
size_of_data = int.from_bytes(old_size_buf, byteorder='big')
logger.info('Size of data with mask: %x mask: %d.', size_of_data, mask)
size_of_data-= (mask<<((size_of_data_size-1)*8))
logger.info('Found element at position: %d, size of type: %d size of data: %d \
maximal size: %d.', initial_position, type_size, size_of_data, max_size)
new_size = size_of_data+addendum
delta = 0
if new_size > max_size:
# TODO: Test this code ...
new_encoded_size = get_ebml_length(new_size)
size_of_new_encoded_size = len(new_encoded_size)
if size_of_new_encoded_size <= size_of_data_size:
logger.error('New encoded size is smaller (%d) or equal than previous size (%d).\
This should not happen.', size_of_new_encoded_size, size_of_data_size)
raise ValueError()
# The difference of length between old size field and new one.
delta = size_of_new_encoded_size - size_of_data_size
file_length = fstat(infd).st_size
# We seek after actual length field
lseek(infd, position+size_of_data_size, SEEK_SET)
# We read the rest of file
tail = read(infd, file_length-(position+size_of_data_size))
# We increase file length
ftruncate(infd, file_length+delta)
# We go to the beginning of length field
lseek(infd, position, SEEK_SET)
# We write the new length field
write(infd, new_encoded_size)
# We overwrite the rest of file with its previous content that has been offset.
write(infd, tail)
else:
size = new_size + ((128>>(size_of_data_size-1))<<((size_of_data_size-1)*8))
new_size_buf = (size).to_bytes(size_of_data_size, byteorder='big')
logger.info('Old encoded size: %s New encoded size: %s', hexdump.dump(old_size_buf,sep=':'),
hexdump.dump(new_size_buf, sep=':'))
lseek(infd, position, SEEK_SET)
write(infd, new_size_buf)
# We return the potential increase in size of the file if the length field had to be increased.
return delta
+3 -471
View File
@@ -6,7 +6,6 @@ import json
import logging import logging
import os.path import os.path
import re import re
from dataclasses import dataclass, field
from datetime import timedelta from datetime import timedelta
from enum import IntEnum, unique from enum import IntEnum, unique
from io import BytesIO, TextIOWrapper from io import BytesIO, TextIOWrapper
@@ -34,22 +33,9 @@ from iso639.exceptions import InvalidLanguageValue
from tqdm import tqdm from tqdm import tqdm
from typeguard import typechecked from typeguard import typechecked
from tscut.h264.bitstream import ( from tscut.h264.avc import (dump_codec_private_data,
read_bit, get_avc_config_from_h264,
read_bits, parse_codec_private)
read_byte,
read_word,
read_long,
write_bits,
write_byte,
write_word,
sodb_to_rbsp,
rbsp_to_sodb,
)
from tscut.h264.parameters import SPS, PPS
# Local modules
# TODO: create local modules for MP4, MKV
# Useful SPS/PPS discussion. # Useful SPS/PPS discussion.
# https://copyprogramming.com/howto/including-sps-and-pps-in-a-raw-h264-track # https://copyprogramming.com/howto/including-sps-and-pps-in-a-raw-h264-track
@@ -455,294 +441,6 @@ def get_codec_private_data_from_mkv(mkvinfo_path:str,
return None, None return None, None
@dataclass
class AVCDecoderConfiguration:
configuration_version:int=1 # u(8)
avc_profile_indication:int=0 # u(8)
profile_compatibility:int=0 # u(8)
avc_level_indication:int=0 # u(8)
length_size_minus_one:int=0 # u(2) (0,1 or 3)
num_of_sequence_parameter_sets:int=0 # u(5)
sps:dict = field(default_factory=dict)
num_of_picture_parameter_sets:int=0 #u(8)
pps:dict = field(default_factory=dict)
chroma_format:int=0 # u(2)
bit_depth_luma_minus8:int=0 # u(3)
bit_depth_chroma_minus8:int=0 # u(3)
num_of_sequence_parameter_set_ext:int=0 # u(8)
spsext:dict = field(default_factory=dict)
def __init__(self):
self.sps = {}
self.spsext = {}
self.pps = {}
def from_bytes(self, buf):
logger = logging.getLogger(__name__)
logger.debug('Parsing: %s', (hexdump.dump(buf,sep=':')))
bit_position = 0
bit_position, self.configuration_version = read_byte(buf, bit_position)
bit_position, self.avc_profile_indication = read_byte(buf, bit_position)
bit_position, self.profile_compatibility = read_byte(buf, bit_position)
bit_position, self.avc_level_indication = read_byte(buf, bit_position)
bit_position, v = read_bits(buf, bit_position, 6)
if v != 0b111111:
raise ValueError(f'Reserved bits are not equal to 0b111111: {v:x}')
bit_position, self.length_size_minus_one = read_bits(buf, bit_position, 2)
bit_position, v = read_bits(buf, bit_position, 3)
if v != 0b111:
raise ValueError(f'Reserved bits are not equal to 0b111: {v:x}')
bit_position, self.num_of_sequence_parameter_sets= read_bits(buf, bit_position, 5)
logger.debug('Number of SPS: %d', self.num_of_sequence_parameter_sets)
for _ in range(self.num_of_sequence_parameter_sets):
bit_position, length = read_word(buf, bit_position)
if bit_position % 8 != 0:
raise ValueError(f'SPS is not located at a byte boundary: {bit_position:d}')
sps = SPS()
sodb = rbsp_to_sodb(buf[floor(bit_position/8):])
bit_length = sps.from_bytes(sodb)
spsid = sps.seq_parameter_set_id
self.sps[spsid] = sps
parsed_length = floor(bit_length/8)
logger.debug('Expected length of SPS: %d bytes. Parsed: %d bytes', length,
parsed_length)
# Parse length can be shorter than length because of rewriting from RBSP to SODB
# (that is shorter).
# So we advance of indicated length.
bit_position+=length*8
logger.debug('Bit position:%d. Reading one byte of: %s', bit_position,
hexdump.dump(buf[floor(bit_position/8):], sep=':'))
bit_position, self.num_of_picture_parameter_sets = read_byte(buf, bit_position)
logger.debug('Number of PPS: %d', self.num_of_picture_parameter_sets)
for _ in range(self.num_of_picture_parameter_sets):
bit_position, length = read_word(buf, bit_position)
if bit_position % 8 != 0:
raise ValueError('PPS is not located at a byte boundary: {bit_position:d}')
pps = PPS()
sodb = rbsp_to_sodb(buf[floor(bit_position/8):])
bit_length = pps.from_bytes(sodb, self.chroma_format)
ppsid = pps.pic_parameter_set_id
self.pps[ppsid] = pps
parsed_length = floor(bit_length/8)
logger.debug('Expected length of PPS: %d bytes. Parsed: %d bytes', length,
parsed_length)
# Parse length can be shorter than length because of rewriting from RBSP to SODB
# (that is shorter).
# So we advance of indicated length.
bit_position+=length*8
logger.debug('Remaining bits: %s', hexdump.dump(buf[floor(bit_position/8):]))
if self.avc_profile_indication in [100, 110, 122, 144]:
bit_position, reserved = read_bits(buf, bit_position, 6)
if reserved != 0b111111:
raise ValueError(f'Reserved bits are different from 111111: {reserved:x}')
bit_position, self.chroma_format = read_bits(buf, bit_position, 2)
bit_position, reserved = read_bits(buf, bit_position, 5)
if reserved != 0b11111:
raise ValueError(f'Reserved bits are different from 11111: {reserved:x}')
bit_position, self.bit_depth_luma_minus8 = read_bits(buf, bit_position, 3)
bit_position, reserved = read_bits(buf, bit_position, 5)
if reserved != 0b11111:
raise ValueError(f'Reserved bits are different from 11111: {reserved:x}')
bit_position, self.bit_depth_chroma_minus8 = read_bits(buf, bit_position, 3)
bit_position, self.num_of_sequence_parameter_set_ext = read_byte(buf, bit_position)
for _ in range(self.num_of_sequence_parameter_set_ext):
# TODO: parse SPSextended
logger.error('Parsing of SPS extended not yet implemented !')
pass
def to_bytes(self):
logger = logging.getLogger(__name__)
buf = bytearray()
bit_position = 0
bit_position = write_byte(buf, bit_position, self.configuration_version)
bit_position = write_byte(buf, bit_position, self.avc_profile_indication)
bit_position = write_byte(buf, bit_position, self.profile_compatibility)
bit_position = write_byte(buf, bit_position, self.avc_level_indication)
bit_position = write_bits(buf, bit_position, 0b111111, 6)
bit_position = write_bits(buf, bit_position, self.length_size_minus_one, 2)
bit_position = write_bits(buf, bit_position, 0b111, 3)
bit_position = write_bits(buf, bit_position, self.num_of_sequence_parameter_sets, 5)
for spsid, sps in self.sps.items():
sodb = sps.to_bytes()
sodb_length = len(sodb)
rbsp = sodb_to_rbsp(sodb)
rbsp_length = len(rbsp)
logger.debug('SODB length: %d RBSP length:%d', sodb_length, rbsp_length)
bit_position = write_word(buf, bit_position, rbsp_length)
buf.extend(rbsp)
bit_position+=rbsp_length*8
logger.debug('2. Buffer: %s', hexdump.dump(buf, sep=':'))
bit_position = write_byte(buf, bit_position, self.num_of_picture_parameter_sets)
for ppsid, lpps in self.pps.items():
logger.debug('Writing PPS: %d', ppsid)
# TODO: does chroma_format should come from self ?
sodb = lpps.to_bytes(self.chroma_format)
sodb_length = len(sodb)
rbsp = sodb_to_rbsp(sodb)
rbsp_length = len(rbsp)
logger.debug('SODB length: %d RBSP length:%d', sodb_length, rbsp_length)
bit_position = write_word(buf, bit_position, rbsp_length)
buf.extend(rbsp)
bit_position+=rbsp_length*8
if self.avc_profile_indication in [ 100, 110, 122, 144]:
bit_position = write_bits(buf, bit_position, 0b111111, 6)
bit_position = write_bits(buf, bit_position, self.chroma_format, 2)
bit_position = write_bits(buf, bit_position, 0b11111, 5)
bit_position = write_bits(buf, bit_position, self.bit_depth_luma_minus8, 3)
bit_position = write_bits(buf, bit_position, 0b11111, 5)
bit_position = write_bits(buf, bit_position, self.bit_depth_chroma_minus8, 3)
bit_position = write_byte(buf, bit_position, self.num_of_sequence_parameter_set_ext)
for _ in range(self.num_of_sequence_parameter_set_ext):
# TODO: dump SPSextended
logger.error('Dumping SPS extended not yet implemented')
pass
return buf
def merge(self, config):
# Check config compatibility
if self.configuration_version != config.configuration_version:
raise ValueError(f'Configuration versions are different: {self.configuration_version:d}\
vs {config.configuration_version:d}')
if self.avc_profile_indication != config.avc_profile_indication:
raise ValueError(f'AVC profiles are different: {self.avc_profile_indication:d} vs \
{config.avc_profile_indication:d}')
if self.profile_compatibility != config.profile_compatibility:
raise ValueError(f'Profile compatilities are different: {self.profile_compatibility:d} \
vs {config.profile_compatibility:d}')
if self.avc_level_indication != config.avc_level_indication:
raise ValueError(f'Level indications are different: {self.avc_level_indication:d} vs \
{config.avc_level_indication:d}')
if self.length_size_minus_one != config.length_size_minus_one:
raise ValueError(f'Length units are different: {self.length_size_minus_one:d} vs \
{config.length_size_minus_one:d}')
if self.chroma_format != config.chroma_format:
raise ValueError(f'Colour format are different: {self.chroma_format:d} vs \
{config.chroma_format:d}')
if self.bit_depth_luma_minus8 != config.bit_depth_luma_minus8:
raise ValueError(f'Depth of luminance are different: {self.bit_depth_luma_minus8:d} vs \
{config.bit_depth_luma_minus8:d}')
if self.bit_depth_chroma_minus8 != config.bit_depth_chroma_minus8:
raise ValueError(f'Depth of chromaticity are different: \
{self.bit_depth_chroma_minus8:d} vs {config.bit_depth_luma_minus8:d}')
for spsid in config.sps:
sps = config.sps[spsid]
if spsid in self.sps:
localsps = self.sps[spsid]
if sps!=localsps:
raise ValueError(f'Profile are not compatible. They contain two different SPS\
with the same identifier ({spsid:d}): {localsps}\n{sps}\n')
self.sps[spsid] = sps
self.num_of_sequence_parameter_sets = len(self.sps)
for ppsid in config.pps:
pps = config.pps[ppsid]
if ppsid in self.pps:
localpps = self.pps[ppsid]
if pps!=localpps:
raise ValueError(f'Profile are not compatible. They contain two different PPS\
with the same identifier ({ppsid:d}): {localpps}\n{pps}\n')
self.pps[ppsid] = pps
self.num_of_picture_parameter_sets = len(self.pps)
# TODO: do the same with extended SPS !
@typechecked
def parse_codec_private(codec_private_data: bytes) -> AVCDecoderConfiguration:
if codec_private_data[0] != 0x63:
raise ValueError(f'Matroska header is wrong: {codec_private_data[0]:x}')
if codec_private_data[1] != 0xA2:
raise ValueError(f'Matroska header is wrong: {codec_private_data[1]:x}')
length = codec_private_data[2]
if length == 0:
raise ValueError('Matroska length cannot start with zero byte.')
for nb_zeroes in range(8):
b = read_bit(codec_private_data[2:], nb_zeroes)
if b != 0:
break
mask = 2^(7-nb_zeroes)-1
length = codec_private_data[2] and mask
for i in range(nb_zeroes):
length*=256
length+=(codec_private_data[3+i])
byte_position = 3+nb_zeroes
avcconfig = AVCDecoderConfiguration()
avcconfig.from_bytes(codec_private_data[byte_position:])
return avcconfig
@typechecked
def get_avc_config_from_h264(input_file: IO[bytes]) -> AVCDecoderConfiguration:
logger = logging.getLogger(__name__)
# TODO: improve this ...
rbsp = input_file.read(1000)
sodb = rbsp_to_sodb(rbsp)
bit_position = 0
bit_position, start_code = read_long(sodb, bit_position)
if start_code != 1:
raise ValueError(f'Starting code not detected: {start_code:x}')
sps = SPS()
bit_length = sps.from_bytes(sodb[4:])
bit_position+=bit_length
bit_position, start_code = read_long(sodb, bit_position)
if start_code != 1:
raise ValueError(f'Starting code not detected: {start_code:x}')
pps = PPS()
bit_length = pps.from_bytes(sodb[floor(bit_position/8):], sps.chroma_format_idc)
logger.debug(pps)
avcconfig = AVCDecoderConfiguration()
avcconfig.configuration_version = 1
avcconfig.avc_profile_indication = sps.profile_idc
avcconfig.profile_compatibility = 0
avcconfig.avc_level_indication = sps.level_idc
avcconfig.length_size_minus_one = 3
avcconfig.num_of_sequence_parameter_sets = 1
avcconfig.num_of_picture_parameter_sets = 1
avcconfig.num_of_sequence_parameter_set_ext = 0
avcconfig.chroma_format = sps.chroma_format_idc
avcconfig.bit_depth_chroma_minus8 = sps.bit_depth_chroma_minus8
avcconfig.bit_depth_luma_minus8 = sps.bit_depth_luma_minus8
avcconfig.sps[sps.seq_parameter_set_id] = sps
avcconfig.pps[pps.pic_parameter_set_id] = pps
return avcconfig
# Unused ?
@typechecked
def get_codec_private_data_from_h264(input_file: IO[bytes]) -> AVCDecoderConfiguration:
avcconfig = get_avc_config_from_h264(input_file)
res = dump_codec_private_data(avcconfig)
return res
@typechecked @typechecked
def parse_mkv_tree(mkvinfo_path:str, input_file: IO[bytes]) -> dict[str,tuple[int,int]]: def parse_mkv_tree(mkvinfo_path:str, input_file: IO[bytes]) -> dict[str,tuple[int,int]]:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -798,173 +496,7 @@ def parse_mkv_tree(mkvinfo_path:str, input_file: IO[bytes]) -> dict[str,tuple[in
mkvinfo.wait() mkvinfo.wait()
return elements return elements
# MKV is formatted as an EBML file (Extended Binary Markup Langage).
# cf http://matroska-org.github.io/libebml/specs.html
# It is a Type, Length, Value (TLV) kind of binary file.
# Types are encoded as follows:
# 1xxx xxxx - Class A IDs (2^7 -1 possible values)
# 01xx xxxx xxxx xxxx - Class B IDs (2^14-1 possible values)
# 001x xxxx xxxx xxxx xxxx xxxx - Class C IDs (2^21-1 possible values)
# 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx - Class D IDs (2^28-1 possible values)
# Lengths are encoded as follows:
# 1xxx xxxx
# value 0 to 2^7-2
# 01xx xxxx xxxx xxxx
# value 0 to 2^14-2
# 001x xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^21-2
# 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^28-2
# 0000 1xxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^35-2
# 0000 01xx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^42-2
# 0000 001x xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^49-2
# 0000 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
# value 0 to 2^56-2
@typechecked
def get_ebml_length(length:int) -> bytes|None:
logger = logging.getLogger(__name__)
if 0 <= length <= 2**7-2:
size = 1
elif length <= 2**14-2:
size = 2
elif length <= 2**21-2:
size = 3
elif length <= 2**28-2:
size = 4
elif length <= 2**35-2:
size = 5
elif length <= 2**42-2:
size = 6
elif length <= 2**49-2:
size = 7
elif length <= 2**56-2:
size = 8
elif length < 0:
logger.error('Impossible to encode a negative length with EBML.')
return None
else:
logger.error('Impossible to encode a length larger than 2^56-2 with EBML.')
return None
encoded_length = length + ((128>>(size-1))<<((size-1)*8))
res = (encoded_length).to_bytes(size, byteorder='big')
return res
@typechecked
def dump_codec_private_data(avc_decoder_configuration: AVCDecoderConfiguration) -> bytearray:
logger = logging.getLogger(__name__)
# Rebuild a Matroska Codec Private Element
res = bytearray()
# Code private element
res.extend(b'\x63\xA2')
buf = avc_decoder_configuration.to_bytes()
logger.debug('AVC configuration bitstream: %s (length: %d))', hexdump.dump(buf, sep=':'),
len(buf))
embl_length = get_ebml_length(len(buf))
logger.debug('EMBL encoded length: %s', hexdump.dump(embl_length, sep=':'))
res.extend(embl_length)
res.extend(buf)
return res
@typechecked
def change_ebml_element_size(input_file: IO[bytes], position:int, addendum:int) -> int:
logger = logging.getLogger(__name__)
initial_position = position
infd = input_file.fileno()
lseek(infd, position, SEEK_SET)
buf = read(infd, 1)
element_type = int.from_bytes(buf, byteorder='big')
mask=128
found = False
for i in range(1,5):
if element_type&mask:
type_size = i
found = True
break
else:
mask = mask>>1
if not found:
logger.error('Size of element type cannot be determined: %d', element_type)
exit(-1)
# We seek to size
position+=type_size
lseek(infd, position, SEEK_SET)
buf = read(infd, 1)
size_head = int.from_bytes(buf, byteorder='big')
logger.info('First byte of size: %x', size_head)
mask=128
found = False
for i in range(1,9):
if size_head&mask:
size_of_data_size = i
found = True
break
else:
mask = mask>>1
if not found:
logger.error('Size of data size cannot be determined: %d', size_head)
exit(-1)
else:
logger.info('Size of data size: %d.', size_of_data_size)
lseek(infd, position, SEEK_SET)
old_size_buf = read(infd, size_of_data_size)
max_size = 2**(size_of_data_size*7)-2
size_of_data = int.from_bytes(old_size_buf, byteorder='big')
logger.info('Size of data with mask: %x mask: %d.', size_of_data, mask)
size_of_data-= (mask<<((size_of_data_size-1)*8))
logger.info('Found element at position: %d, size of type: %d size of data: %d \
maximal size: %d.', initial_position, type_size, size_of_data, max_size)
new_size = size_of_data+addendum
delta = 0
if new_size > max_size:
# TODO: Test this code ...
new_encoded_size = get_ebml_length(new_size)
size_of_new_encoded_size = len(new_encoded_size)
if size_of_new_encoded_size <= size_of_data_size:
logger.error('New encoded size is smaller (%d) or equal than previous size (%d).\
This should not happen.', size_of_new_encoded_size, size_of_data_size)
exit(-1)
# The difference of length between old size field and new one.
delta = size_of_new_encoded_size - size_of_data_size
file_length = fstat(infd).st_size
# We seek after actual length field
lseek(infd, position+size_of_data_size, SEEK_SET)
# We read the rest of file
tail = read(infd, file_length-(position+size_of_data_size))
# We increase file length
ftruncate(infd, file_length+delta)
# We go to the beginning of length field
lseek(infd, position, SEEK_SET)
# We write the new length field
write(infd, new_encoded_size)
# We overwrite the rest of file with its previous content that has been offset.
write(infd, tail)
else:
size = new_size + ((128>>(size_of_data_size-1))<<((size_of_data_size-1)*8))
new_size_buf = (size).to_bytes(size_of_data_size, byteorder='big')
logger.info('Old encoded size: %s New encoded size: %s', hexdump.dump(old_size_buf,sep=':'),
hexdump.dump(new_size_buf, sep=':'))
lseek(infd, position, SEEK_SET)
write(infd, new_size_buf)
# We return the potential increase in size of the file if the length field had to be increased.
return delta
@typechecked @typechecked
def change_codec_private_data(mkvinfo_path:str, input_file: IO[bytes], codec_data:bytes) -> None: def change_codec_private_data(mkvinfo_path:str, input_file: IO[bytes], codec_data:bytes) -> None: