From c3943ff70eedae22df09fa1cb182e1324b8f52b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Tronel?= Date: Sat, 25 Oct 2025 16:33:29 +0200 Subject: [PATCH] Remove trailing spaces. --- removeads.py | 677 +++++++++++++++++++++++++-------------------------- 1 file changed, 327 insertions(+), 350 deletions(-) diff --git a/removeads.py b/removeads.py index 9c37375..c011f71 100755 --- a/removeads.py +++ b/removeads.py @@ -400,7 +400,7 @@ def readUnsignedExpGolomb(buf, bitPosition): if b!=0: break nbZeroes+=1 - + v1 = 1 bitPosition, v2 = readBits(buf, bitPosition, nbZeroes) v = (v1<>1) @@ -417,17 +417,17 @@ def readSignedExpGolomb(buf, bitPosition): def writeBit(buf, bitPosition, b): logger = logging.getLogger(__name__) - + bufLength = len(buf) bytePosition = floor(bitPosition/8) - + if bytePosition >= bufLength: extension = bytearray(bytePosition+1-bufLength) buf.extend(extension) - + buf[bytePosition] |= (b<<(7-(bitPosition % 8))) bitPosition+=1 - + return bitPosition def writeBoolean(buf, bitPosition, b): @@ -463,7 +463,7 @@ def writeUnsignedExpGolomb(buf, bitPosition, v): bitPosition = writeBits(buf, bitPosition, 0, n-1) bitPosition = writeBit(buf, bitPosition, 1) bitPosition = writeBits(buf, bitPosition, v+1, n-1) - + return bitPosition def writeSignedExpGolomb(buf, bitPosition, v): @@ -471,13 +471,13 @@ def writeSignedExpGolomb(buf, bitPosition, v): bitPosition = writeUnsignedExpGolomb(buf, bitPosition, -v*2) else: bitPosition = writeUnsignedExpGolomb(buf, bitPosition, v*2-1) - + return bitPosition def parseRBSPTrailingBits(buf, bitPosition): logger = logging.getLogger(__name__) - + bitPosition, one = readBit(buf, bitPosition) if one==0: raise(Exception('Stop bit should be equal to one. Read: %d' % one)) @@ -485,23 +485,23 @@ def parseRBSPTrailingBits(buf, bitPosition): bitPosition, zero = readBit(buf, bitPosition) if zero==1: raise(Exception('Trailing bit should be equal to zero')) - + return bitPosition def writeRBSPTrailingBits(buf, bitPosition): bitPosition = writeBit(buf, bitPosition, 1) while bitPosition%8 != 0: bitPosition = writeBit(buf, bitPosition, 0) - + return bitPosition def moreRBSPData(buf, bitPosition): logger = logging.getLogger(__name__) logger.debug('Is there more data in buffer of length: %d at bitPosition: %d' % (len(buf), bitPosition)) - + byteLength = len(buf) bitLength = byteLength*8 - + # We are at the end of buffer if bitPosition == bitLength: return False @@ -512,14 +512,14 @@ def moreRBSPData(buf, bitPosition): if b == 1: found = True break - + if not found: raise(Exception('Impossible to find trailing stop bit !')) - + # No more data if bitPosition == pos: return False - + return True # Convert from RBSP (Raw Byte Sequence Payload) to SODB (String Of Data Bits) @@ -527,7 +527,7 @@ def RBSP2SODB(buf): logger = logging.getLogger(__name__) logger.debug('RBSP: %s' % hexdump.dump(buf, sep=':')) - + res = buf for b in [ b'\x00', b'\x01', b'\x02', b'\x03']: pattern = b'\x00\x00\x03'+b @@ -547,14 +547,14 @@ def SODB2RBSP(buf): pattern = b'\x00\x00'+b replacement = b'\x00\x00\x03' + b res = res.replace(pattern, replacement) - - logger.debug('RBSP: %s' % hexdump.dump(res, sep=':')) + + logger.debug('RBSP: %s', hexdump.dump(res, sep=':')) return res - + # Useful for SPS and PPS def parseScalingList(buf, bitPosition, size): logger = logging.getLogger(__name__) - + res = [] lastScale = 8 nextScale = 8 @@ -565,15 +565,15 @@ def parseScalingList(buf, bitPosition, size): v = lastScale if nextScale==0 else nextScale res.append(v) lastScale = v - - return bitPosition,res + + return bitPosition,res # TODO: test optimized version. # The ISO/IEC H.264-201602 seems to take into account the case where the end of the deltas list is full of zeroes. def writeScalingList(buf, bitPosition, size, matrix, optimized=False): logger = logging.getLogger(__name__) logger.debug('Dumping matrix: %s of size: %d, size parameter: %d.', matrix, len(matrix), size) - + prev = 8 deltas = [] for i in range(0, size): @@ -581,7 +581,7 @@ def writeScalingList(buf, bitPosition, size, matrix, optimized=False): delta = v - prev deltas.append(delta) prev = v - + if not optimized: for delta in deltas: bitPosition = writeSignedExpGolomb(buf, bitPosition, delta) @@ -621,7 +621,7 @@ class HRD: self.bit_rate_value_minus1 = {} self.cpb_size_value_minus1 = {} self.cbr_flag = {} - + def fromBytes(self, buf, bitPosition): bitPosition, self.cpb_cnt_minus1 = readUnsignedExpGolomb(buf, bitPosition) bitPosition, self.bit_rate_scale = readBits(buf, bitPosition, 4) @@ -637,9 +637,9 @@ class HRD: bitPosition, self.cpb_removal_delay_length_minus1 = readBits(buf, bitPosition, 5) bitPosition, self.dpb_output_delay_length_minus1 = readBits(buf, bitPosition, 5) bitPosition, self.time_offset_length = readBits(buf, bitPosition, 5) - + return bitPosition - + def toBytes(self, buf, bitPosition): bitPosition = writeUnsignedExpGolomb(buf, bitPosition, self.cpb_cnt_minus1) bitPosition = writeBits(buf, bitPosition, self.bit_rate_scale, 4) @@ -655,9 +655,8 @@ class HRD: bitPosition = writeBits(buf, bitPosition, self.cpb_removal_delay_length_minus1, 5) bitPosition = writeBits(buf, bitPosition, self.dpb_output_delay_length_minus1, 5) bitPosition = writeBits(buf, bitPosition, self.time_offset_length, 5) - + return bitPosition - @dataclass class VUI: @@ -695,12 +694,12 @@ class VUI: log2_max_mv_length_vertical:int=0 max_num_reorder_frames:int=0 max_dec_frame_buffering:int=0 - + # This structure is not guaranteed to be located at a byte boundary. # We must explicitely indicate bit offset. def fromBytes(self, buf, bitPosition): logger = logging.getLogger(__name__) - + bitPosition, self.aspect_ratio_info_present_flag = readBoolean(buf, bitPosition) if self.aspect_ratio_info_present_flag: bitPosition, self.aspect_ratio_idc = readByte(buf, bitPosition) @@ -750,12 +749,12 @@ class VUI: bitPosition, self.log2_max_mv_length_vertical = readUnsignedExpGolomb(buf, bitPosition) bitPosition, self.max_num_reorder_frames = readUnsignedExpGolomb(buf, bitPosition) bitPosition, self.max_dec_frame_buffering = readUnsignedExpGolomb(buf, bitPosition) - + return bitPosition - + def toBytes(self, buf, bitPosition): logger = logging.getLogger(__name__) - + bitPosition = writeBoolean(buf, bitPosition, self.aspect_ratio_info_present_flag) if self.aspect_ratio_info_present_flag: bitPosition = writeByte(buf, bitPosition, self.aspect_ratio_idc) @@ -801,9 +800,9 @@ class VUI: bitPosition = writeUnsignedExpGolomb(buf, bitPosition, self.log2_max_mv_length_vertical) bitPosition = writeUnsignedExpGolomb(buf, bitPosition, self.max_num_reorder_frames) bitPosition = writeUnsignedExpGolomb(buf, bitPosition, self.max_dec_frame_buffering) - + return bitPosition - + @dataclass class SPS: profile_idc:int=0 # u(8) @@ -848,16 +847,16 @@ class SPS: def __init__(self): self.scaling_list={} self.offset_for_ref_frame={} - + # TODO: ... # Compute options to pass to ffmpeg so as to reproduce the same SPS. # Very complex since some codec configuration are not provided by ffmpeg and/or libx264. # This is only an attempt. - + def ffmpegOptions(self, videoID=0): logger = logging.getLogger(__name__) x264opts = [] - + if self.profile_idc in [ 0x42, 0x4D, 0x64, 0x6E, 0x7A, 0xF4, 0x2C]: if self.profile_idc == 0x42: profile = 'baseline' @@ -877,11 +876,11 @@ class SPS: level = '%d.%d' % (floor(self.level_idc/10), self.level_idc % 10) x264opts.extend(['sps-id=%d' % self.seq_parameter_set_id] ) - + if self.bit_depth_chroma_minus8 not in [0,1,2,4,6,8]: logger.error('Bit depth of chrominance is not supported: %d', self.bit_depth_chroma_minus8+8) return [] - + if self.chroma_format_idc in range(0,4): if self.chroma_format_idc == 0: # Monochrome @@ -898,16 +897,16 @@ class SPS: else: logger.error('Unknow chrominance format: %x', self.chroma_format_idc) return [] - + res = ['-profile:v:%d' % videoID, self.profile_idc, '-level:v:%d' % videoID, level] return res - + def fromBytes(self, buf): logger = logging.getLogger(__name__) logger.debug('Parsing: %s' % (hexdump.dump(buf,sep=':'))) - + bitPosition=0 - + # NAL Unit SPS bitPosition, zero = readBit(buf, bitPosition) if zero != 0: @@ -918,7 +917,7 @@ class SPS: bitPosition, nal_unit_type = readBits(buf, bitPosition,5) if nal_unit_type != 7: raise(Exception('NAL unit type is not a SPS: %d' % nal_unit_type )) - + bitPosition, self.profile_idc = readByte(buf, bitPosition) bitPosition, self.constraint_set0_flag = readBit(buf,bitPosition) bitPosition, self.constraint_set1_flag = readBit(buf,bitPosition) @@ -952,7 +951,7 @@ class SPS: self.scaling_list[i] = matrix else: self.scaling_list[i] = [] - + bitPosition, self.log2_max_frame_num_minus4 = readUnsignedExpGolomb(buf, bitPosition) bitPosition , self.pic_order_cnt_type = readUnsignedExpGolomb(buf, bitPosition) if self.pic_order_cnt_type == 0: @@ -965,7 +964,7 @@ class SPS: for i in range(0, self.num_ref_frames_in_pic_order_cnt_cycle): bitPosition, v = readUnsignedExpGolomb(buf, bitPosition) self.offset_for_ref_frame[i]=v - + bitPosition, self.max_num_ref_frames = readUnsignedExpGolomb(buf, bitPosition) bitPosition, self.gaps_in_frame_num_value_allowed_flag = readBoolean(buf, bitPosition) bitPosition, self.pic_width_in_mbs_minus1 = readUnsignedExpGolomb(buf, bitPosition) @@ -981,21 +980,20 @@ class SPS: bitPosition, self.frame_crop_top_offset = readUnsignedExpGolomb(buf, bitPosition) bitPosition, self.frame_crop_bottom_offset = readUnsignedExpGolomb(buf, bitPosition) bitPosition, self.vui_parameters_present_flag = readBoolean(buf, bitPosition) - + if self.vui_parameters_present_flag: self.vui = VUI() bitPosition = self.vui.fromBytes(buf,bitPosition) logger.debug('VUI present: %s', self.vui) - - + logger.debug('Parse end of SPS. Bit position: %d. Remaining bytes: %s.' % (bitPosition, hexdump.dump(buf[floor(bitPosition/8):], sep=':'))) bitPosition = parseRBSPTrailingBits(buf, bitPosition) logger.debug('End of SPS: %d. Remaining bytes: %s' % (bitPosition, hexdump.dump(buf[floor(bitPosition/8):], sep=':'))) return bitPosition - + def toBytes(self): logger = logging.getLogger(__name__) - + buf = bytearray() bitPosition = 0 bitPosition = writeBit(buf, bitPosition,0) @@ -1062,11 +1060,11 @@ class SPS: logger.debug('SPS has VUI. Writing VUI at position: %d', bitPosition) bitPosition = self.vui.toBytes(buf, bitPosition) logger.debug('VUI written. New bit position: %d', bitPosition) - + bitPosition = writeRBSPTrailingBits(buf, bitPosition) - + return buf - + @dataclass class PPS: pic_parameter_set_id:int=0 @@ -1096,19 +1094,19 @@ class PPS: pic_scaling_matrix_present_flag:bool=False pic_scaling_list:list[list[int]] = field(default_factory=list) second_chroma_qp_index_offset:int=0 - + def __init__(self): self.run_length_minus1={} self.top_left={} self.bottom_right={} self.slice_group_id={} self.pic_scaling_list=[] - + # PPS are located at byte boundary def fromBytes(self, buf, chroma_format_idc): logger = logging.getLogger(__name__) logger.debug('Parsing: %s' % (hexdump.dump(buf,sep=':'))) - + bitPosition=0 # NAL Unit PPS bitPosition, zero = readBit(buf, bitPosition) @@ -1120,13 +1118,13 @@ class PPS: bitPosition, nal_unit_type = readBits(buf, bitPosition,5) if nal_unit_type != 8: raise(Exception('NAL unit type is not a PPS: %d' % nal_unit_type )) - + bitPosition, self.pic_parameter_set_id = readUnsignedExpGolomb(buf, bitPosition) bitPosition, self.seq_parameter_set_id = readUnsignedExpGolomb(buf, bitPosition) bitPosition, self.entropy_coding_mode_flag = readBoolean(buf, bitPosition) bitPosition, self.bottom_field_pic_order_in_frame_present_flag = readBoolean(buf, bitPosition) bitPosition, self.num_slice_groups_minus1 = readUnsignedExpGolomb(buf, bitPosition) - + if self.num_slice_groups_minus1>0: bitPosition, self.slice_group_map_type = readUnsignedExpGolomb(buf, bitPosition) if self.slice_group_map_type == 0: @@ -1148,7 +1146,7 @@ class PPS: for i in range(0, self.pic_size_in_map_units_minus1): bitPosition, v = readBits(buf, bitPosition, l) self.slice_group_id[i]=v - + bitPosition, self.num_ref_idx_l0_default_active_minus1 = readUnsignedExpGolomb(buf, bitPosition) bitPosition, self.num_ref_idx_l2_default_active_minus1 = readUnsignedExpGolomb(buf, bitPosition) bitPosition, self.weighted_pred_flag = readBoolean(buf, bitPosition) @@ -1159,7 +1157,7 @@ class PPS: bitPosition, self.deblocking_filter_control_present_flag = readBoolean(buf, bitPosition) bitPosition, self.constrained_intra_pred_flag = readBoolean(buf, bitPosition) bitPosition, self.redundant_pic_cnt_present_flag = readBoolean(buf, bitPosition) - + if moreRBSPData(buf, bitPosition): bitPosition, self.transform_8x8_mode_flag = readBoolean(buf, bitPosition) bitPosition, self.pic_scaling_matrix_present_flag = readBoolean(buf, bitPosition) @@ -1181,28 +1179,28 @@ class PPS: else: self.pic_scaling_list.append([]) bitPosition, self.second_chroma_qp_index_offset = readSignedExpGolomb(buf, bitPosition) - + logger.info("parse RBSP") bitPosition = parseRBSPTrailingBits(buf, bitPosition) - + return bitPosition - + def toBytes(self, chroma_format_idc): logger = logging.getLogger(__name__) - + buf = bytearray() bitPosition = 0 # NAL Unit PPS bitPosition = writeBit(buf, bitPosition, 0) bitPosition = writeBits(buf, bitPosition, 3, 2) bitPosition = writeBits(buf, bitPosition, 8, 5) - + bitPosition = writeUnsignedExpGolomb(buf, bitPosition, self.pic_parameter_set_id) bitPosition = writeUnsignedExpGolomb(buf, bitPosition, self.seq_parameter_set_id) bitPosition = writeBoolean(buf, bitPosition, self.entropy_coding_mode_flag) bitPosition = writeBoolean(buf, bitPosition, self.bottom_field_pic_order_in_frame_present_flag) bitPosition = writeUnsignedExpGolomb(buf, bitPosition, self.num_slice_groups_minus1) - + if self.num_slice_groups_minus1>0: bitPosition = writeUnsignedExpGolomb(buf, bitPosition, self.slice_group_map_type) if self.slice_group_map_type == 0: @@ -1224,7 +1222,7 @@ class PPS: for i in range(0, self.pic_size_in_map_units_minus1): v = self.slice_group_id[i] bitPosition, v = writeBits(buf, bitPosition, v, l) - + bitPosition = writeUnsignedExpGolomb(buf, bitPosition, self.num_ref_idx_l0_default_active_minus1) bitPosition = writeUnsignedExpGolomb(buf, bitPosition, self.num_ref_idx_l2_default_active_minus1) bitPosition = writeBoolean(buf, bitPosition, self.weighted_pred_flag) @@ -1235,7 +1233,7 @@ class PPS: bitPosition = writeBoolean(buf, bitPosition, self.deblocking_filter_control_present_flag) bitPosition = writeBoolean(buf, bitPosition, self.constrained_intra_pred_flag) bitPosition = writeBoolean(buf, bitPosition, self.redundant_pic_cnt_present_flag) - + bitPosition = writeBoolean(buf, bitPosition, self.transform_8x8_mode_flag) bitPosition = writeBoolean(buf, bitPosition, self.pic_scaling_matrix_present_flag) if self.pic_scaling_matrix_present_flag: @@ -1258,11 +1256,11 @@ class PPS: logger.info("Writing matrix: %s" % matrix) bitPosition = writeScalingList(buf, bitPosition, 64, matrix) bitPosition = writeSignedExpGolomb(buf, bitPosition, self.second_chroma_qp_index_offset) - + bitPosition = writeRBSPTrailingBits(buf, bitPosition) - + return buf - + @dataclass class AVCDecoderConfiguration: configurationVersion:int=1 # u(8) @@ -1279,12 +1277,12 @@ class AVCDecoderConfiguration: bit_depth_chroma_minus8:int=0 # u(3) numOfSequenceParameterSetExt:int=0 # u(8) spsext:dict = field(default_factory=dict) - + def __init__(self): self.sps = {} self.spsext = {} self.pps = {} - + def fromBytes(self, buf): logger = logging.getLogger(__name__) logger.debug('Parsing: %s' % (hexdump.dump(buf,sep=':'))) @@ -1312,13 +1310,13 @@ class AVCDecoderConfiguration: bitLength = sps.fromBytes(sodb) spsid = sps.seq_parameter_set_id self.sps[spsid] = sps - + parsedLength = floor(bitLength/8) logger.debug('Expected length of SPS: %d bytes. Parsed: %d bytes' % (length, parsedLength)) # Parse length can be shorter than length because of rewriting from RBSP to SODB (that is shorter). # So we advance of indicated length. bitPosition+=length*8 - + logger.debug('Bit position:%d. Reading one byte of: %s' % (bitPosition, hexdump.dump(buf[floor(bitPosition/8):], sep=':'))) bitPosition, self.numOfPictureParameterSets = readByte(buf, bitPosition) logger.debug('Number of PPS: %d' % self.numOfPictureParameterSets) @@ -1326,22 +1324,21 @@ class AVCDecoderConfiguration: bitPosition, length = readWord(buf, bitPosition) if bitPosition % 8 != 0: raise(Exception('PPS is not located at a byte boundary: %d' % bitPosition )) - + pps = PPS() sodb = RBSP2SODB(buf[floor(bitPosition/8):]) bitLength = pps.fromBytes(sodb, self.chroma_format) ppsid = pps.pic_parameter_set_id self.pps[ppsid] = pps - + parsedLength = floor(bitLength/8) logger.debug('Expected length of PPS: %d bytes. Parsed: %d bytes' % (length, parsedLength)) # Parse length can be shorter than length because of rewriting from RBSP to SODB (that is shorter). # So we advance of indicated length. bitPosition+=length*8 - + logger.debug('Remaining bits: %s' % hexdump.dump(buf[floor(bitPosition/8):])) - - + if self.AVCProfileIndication in [100, 110, 122, 144]: bitPosition, reserved = readBits(buf, bitPosition, 6) if reserved != 0b111111: @@ -1365,7 +1362,7 @@ class AVCDecoderConfiguration: def toBytes(self): logger = logging.getLogger(__name__) - + buf = bytearray() bitPosition = 0 bitPosition = writeByte(buf, bitPosition, self.configurationVersion) @@ -1382,15 +1379,15 @@ class AVCDecoderConfiguration: sodbLength = len(sodb) rbsp = SODB2RBSP(sodb) rbspLength = len(rbsp) - + logger.debug('SODB length: %d RBSP length:%d' % (sodbLength, rbspLength)) - + bitPosition = writeWord(buf, bitPosition, rbspLength) buf.extend(rbsp) bitPosition+=rbspLength*8 - + logger.debug('2. Buffer: %s' % hexdump.dump(buf, sep=':')) - + bitPosition = writeByte(buf, bitPosition, self.numOfPictureParameterSets) for ppsid in self.pps: logger.debug('Writing PPS: %d' % ppsid) @@ -1400,13 +1397,13 @@ class AVCDecoderConfiguration: sodbLength = len(sodb) rbsp = SODB2RBSP(sodb) rbspLength = len(rbsp) - + logger.debug('SODB length: %d RBSP length:%d' % (sodbLength, rbspLength)) - + bitPosition = writeWord(buf, bitPosition, rbspLength) buf.extend(rbsp) bitPosition+=rbspLength*8 - + if self.AVCProfileIndication in [ 100, 110, 122, 144]: bitPosition = writeBits(buf, bitPosition, 0b111111, 6) bitPosition = writeBits(buf, bitPosition, self.chroma_format, 2) @@ -1421,7 +1418,7 @@ class AVCDecoderConfiguration: pass return buf - + def merge(self, config): # Check config compatibility if self.configurationVersion != config.configurationVersion: @@ -1440,7 +1437,7 @@ class AVCDecoderConfiguration: raise(Exception('Depth of luminance are different: %d vs %s' % (self.bit_depth_luma_minus8, config.bit_depth_luma_minus8))) if self.bit_depth_chroma_minus8 != config.bit_depth_chroma_minus8: raise(Exception('Depth of chromaticity are different: %d vs %s' % (self.bit_depth_chroma_minus8, config.bit_depth_luma_minus8))) - + for spsid in config.sps: sps = config.sps[spsid] if spsid in self.sps: @@ -1448,9 +1445,9 @@ class AVCDecoderConfiguration: if sps!=localsps: raise(Exception('Profile are not compatible. They contain two different SPS with the same identifier (%d): %s\n%s\n' % (spsid, localsps, sps))) self.sps[spsid] = sps - + self.numOfSequenceParameterSets = len(self.sps) - + for ppsid in config.pps: pps = config.pps[ppsid] if ppsid in self.pps: @@ -1458,12 +1455,11 @@ class AVCDecoderConfiguration: if pps!=localpps: raise(Exception('Profile are not compatible. They contain two different PPS with the same identifier (%d): %s\n%s\n' % (ppsid, localpps, pps))) self.pps[ppsid] = pps - + self.numOfPictureParameterSets = len(self.pps) - + # TODO: do the same with extended SPS ! - - + def parseCodecPrivate(codecPrivateData): if codecPrivateData[0] != 0x63: raise(Exception('Matroska header is wrong: %x' % codecPrivateData[0])) @@ -1484,16 +1480,16 @@ def parseCodecPrivate(codecPrivateData): bytePosition = 3+nbZeroes avcconfig = AVCDecoderConfiguration() avcconfig.fromBytes(codecPrivateData[bytePosition:]) - + return avcconfig def getAvcConfigFromH264(inputFile): logger = logging.getLogger(__name__) - + # TODO: improve this ... rbsp = inputFile.read(1000) sodb = RBSP2SODB(rbsp) - + bitPosition = 0 bitPosition, startCode = readLong(sodb, bitPosition) if startCode != 1: @@ -1501,14 +1497,14 @@ def getAvcConfigFromH264(inputFile): sps = SPS() bitLength = sps.fromBytes(sodb[4:]) bitPosition+=bitLength - + bitPosition, startCode = readLong(sodb, bitPosition) if startCode != 1: raise(Exception('Starting code not detected: %x' % startCode)) pps = PPS() bitLength = pps.fromBytes(sodb[floor(bitPosition/8):], sps.chroma_format_idc) logger.debug(pps) - + avcconfig = AVCDecoderConfiguration() avcconfig.configurationVersion = 1 avcconfig.AVCProfileIndication = sps.profile_idc @@ -1523,26 +1519,26 @@ def getAvcConfigFromH264(inputFile): 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 def getCodecPrivateDataFromH264(inputFile): logger = logging.getLogger(__name__) - + avcconfig = getAvcConfigFromH264(inputFile) res = dumpCodecPrivateData(avcconfig) - + return res def parseMKVTree(mkvinfo, inputFile): logger = logging.getLogger(__name__) - + infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) set_inheritable(infd, True) env = {**os.environ, 'LANG': 'C'} elements = {} - + with Popen([mkvinfo, '-z', '-X', '-P', '/proc/self/fd/%d' % infd ], stdout=PIPE, close_fds=False, env=env) as mkvinfo: out, _ = mkvinfo.communicate() out = out.decode('utf8') @@ -1562,7 +1558,7 @@ def parseMKVTree(mkvinfo, inputFile): depth = 0 else: depth = len(m.group('depth')) - + if depth > prevDepth: for i in range(depth-prevDepth): prefix.append(1) @@ -1578,12 +1574,11 @@ def parseMKVTree(mkvinfo, inputFile): subid+=1 prefix.pop() prefix.append(subid) - + prevDepth = depth key=".".join(map(str, prefix)) - elements[key] = (position, size) - + mkvinfo.wait() return elements @@ -1608,7 +1603,7 @@ def parseMKVTree(mkvinfo, inputFile): def getEBMLLength(length): logger = logging.getLogger(__name__) - + if (0 <= length) and (length <= 2**7-2): size = 1 elif length <= 2**14-2: @@ -1631,7 +1626,7 @@ def getEBMLLength(length): else: logger.error('Impossible to encode a length larger than 2^56-2 with EBML.') return None - + encodedLength = length + ((128>>(size-1))<<((size-1)*8)) res = (encodedLength).to_bytes(size, byteorder='big') return res @@ -1645,23 +1640,22 @@ def dumpCodecPrivateData(AVCDecoderConfiguration): res.extend(b'\x63\xA2') buf = AVCDecoderConfiguration.toBytes() logger.debug('AVC configuration bitstream: %s (length: %d))' % (hexdump.dump(buf, sep=':'), len(buf))) - + EMBLlength = getEBMLLength(len(buf)) logger.debug('EMBL encoded length: %s' % (hexdump.dump(EMBLlength, sep=':'))) res.extend(EMBLlength) res.extend(buf) - + return res - def changeEBMLElementSize(inputFile, position, addendum): logger = logging.getLogger(__name__) - + initialPosition = position infd = inputFile.fileno() lseek(infd, position, SEEK_SET) - + buf = read(infd, 1) elementType = int.from_bytes(buf, byteorder='big') mask=128 @@ -1673,11 +1667,11 @@ def changeEBMLElementSize(inputFile, position, addendum): break else: mask = mask>>1 - + if not found: logger.error('Size of element type cannot be determined: %d', elementType) exit(-1) - + # We seek to size position+=typeSize lseek(infd, position, SEEK_SET) @@ -1700,15 +1694,15 @@ def changeEBMLElementSize(inputFile, position, addendum): exit(-1) else: logger.info('Size of data size: %d.', sizeOfDataSize) - + lseek(infd, position, SEEK_SET) oldSizeBuf = read(infd, sizeOfDataSize) maxSize = 2**(sizeOfDataSize*7)-2 sizeOfData = int.from_bytes(oldSizeBuf, byteorder='big') logger.info('Size of data with mask: %x mask: %d.' % (sizeOfData, mask)) sizeOfData-= (mask<<((sizeOfDataSize-1)*8)) - logger.info('Found element at position: %d, size of type: %d size of data: %d maximal size: %d.' % (initialPosition, typeSize, sizeOfData, maxSize)) - + logger.info('Found element at position: %d, size of type: %d size of data: %d maximal size: %d.', initialPosition, typeSize, sizeOfData, maxSize) + newSize = sizeOfData+addendum delta = 0 if newSize > maxSize: @@ -1716,7 +1710,7 @@ def changeEBMLElementSize(inputFile, position, addendum): newEncodedSize = getEBMLLength(newSize) sizeOfNewEncodedSize = len(newEncodedSize) if sizeOfNewEncodedSize <= sizeOfDataSize: - logger.error('New encoded size is smaller (%d) or equal than previous size (%d). This should not happen.' % (sizeOfNewEncodedSize, sizeOfDataSize)) + logger.error('New encoded size is smaller (%d) or equal than previous size (%d). This should not happen.' , sizeOfNewEncodedSize, sizeOfDataSize) exit(-1) # The difference of length between old size field and new one. delta = sizeOfNewEncodedSize - sizeOfDataSize @@ -1740,28 +1734,28 @@ def changeEBMLElementSize(inputFile, position, addendum): logger.info('Old encoded size: %s New encoded size: %s' % (hexdump.dump(oldSizeBuf,sep=':'), hexdump.dump(newSizeBuf, sep=':'))) lseek(infd, position, SEEK_SET) write(infd, newSizeBuf) - + # We return the potential increase in size of the file if the length field had to be increased. return delta def changeCodecPrivateData(mkvinfo, inputFile, codecData): logger = logging.getLogger(__name__) - + infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) - + currentLength = fstat(infd).st_size logger.info('Current size of file: %d' % currentLength) position, currentData = getCodecPrivateDataFromMKV(mkvinfo, inputFile) currentDataLength = len(currentData) futureLength = currentLength - currentDataLength + len(codecData) logger.info('Expected size of file: %d' % futureLength) - + logger.info('Current data at position %d: %s' % (position, hexdump.dump(currentData, sep=":"))) logger.info('Future data: %s' % hexdump.dump(codecData, sep=":")) elements = parseMKVTree(mkvinfo, inputFile) - + found = False for key in elements: pos, size = elements[key] @@ -1773,7 +1767,7 @@ def changeCodecPrivateData(mkvinfo, inputFile, codecData): if not found: logger.error('Impossible to retrieve the key of codec private data') exit(-1) - + if currentLength < futureLength: lseek(infd, position+currentDataLength, SEEK_SET) tail = read(infd, currentLength-(position+currentDataLength)) @@ -1800,7 +1794,7 @@ def changeCodecPrivateData(mkvinfo, inputFile, codecData): # We have to modify the tree elements up to the root that contains the codec private data. keys = key.split('.') logger.info(keys) - + delta = futureLength-currentLength # if there is no modification of the private codec data, no need to change anything. if delta != 0: @@ -1812,11 +1806,10 @@ def changeCodecPrivateData(mkvinfo, inputFile, codecData): # Changing an element can increase its size (in very rare case). # In that case, we update the new delta that will be larger (because the element has been resized). delta+=changeEBMLElementSize(inputFile, pos, delta) - def getFormat(ffprobe, inputFile): logger = logging.getLogger(__name__) - + infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) set_inheritable(infd, True) @@ -1833,7 +1826,7 @@ def getFormat(ffprobe, inputFile): def getMovieDuration(ffprobe, inputFile): logger = logging.getLogger(__name__) - + infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) set_inheritable(infd, True) @@ -1852,7 +1845,7 @@ def getMovieDuration(ffprobe, inputFile): # ffprobe -loglevel quiet -select_streams v:0 -show_entries stream=width,height -of json ./example.ts def getVideoDimensions(ffprobe, inputFile): logger = logging.getLogger(__name__) - + infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) set_inheritable(infd, True) @@ -1863,14 +1856,14 @@ def getVideoDimensions(ffprobe, inputFile): video = out['streams'][0] if ('width' in video) and ('height' in video): return int(video['width']), int(video['height']) - + logger.error('Impossible to retrieve dimensions of video') exit(-1) def getStreams(ffprobe, inputFile): logger = logging.getLogger(__name__) - + infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) set_inheritable(infd, True) @@ -1881,12 +1874,12 @@ def getStreams(ffprobe, inputFile): return out['streams'] else: logger.error('Impossible to retrieve streams inside file') - + return None def withSubtitles(ffprobe, inputFile): logger = logging.getLogger(__name__) - + infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) set_inheritable(infd, True) @@ -1900,19 +1893,19 @@ def withSubtitles(ffprobe, inputFile): return True else: logger.error('Impossible to retrieve streams inside file') - + return False def parseTimestamp(ts): logger = logging.getLogger(__name__) - + tsRegExp = r'^(?P[0-9]{1,2}):(?P[0-9]{1,2}):(?P[0-9]{1,2})(\.(?P[0-9]{1,6}))?$' p = re.compile(tsRegExp) m = p.match(ts) if m is None: logger.warning("Impossible to parse timestamp: %s" % ts) return None - + values = m.groupdict() hour = 0 minute = 0 @@ -1926,7 +1919,7 @@ def parseTimestamp(ts): second = int(values['second']) if values['us'] is not None: us = int(values['us']) - + if hour < 0 or hour > 23: logger.error("hour must be in [0,24[") return None @@ -1939,21 +1932,20 @@ def parseTimestamp(ts): if us < 0 or us > 1000000: logger.error("milliseconds must be in [0,1000000[") return None - ts = timedelta(hours=hour, minutes=minute, seconds=second, microseconds=us) return ts def parseTimeInterval(interval): logger = logging.getLogger(__name__) - + intervalRegExp = r'^(?P[0-9]{1,2}):(?P[0-9]{1,2}):(?P[0-9]{1,2})(\.(?P[0-9]{1,3}))?-(?P[0-9]{1,2}):(?P[0-9]{1,2}):(?P[0-9]{1,2})(\.(?P[0-9]{1,3}))?$' p = re.compile(intervalRegExp) m = p.match(interval) if m is None: logger.error("Impossible to parse time interval") return None - + values = m.groupdict() hour1 = 0 minute1 = 0 @@ -1979,7 +1971,7 @@ def parseTimeInterval(interval): second2 = int(values['second2']) if values['ms2'] is not None: ms2 = int(values['ms2']) - + if hour1 < 0 or hour1 > 23: logger.error("hour must be in [0,24[") return None, None @@ -1992,7 +1984,7 @@ def parseTimeInterval(interval): if ms1 < 0 or ms1 > 1000: logger.error("milliseconds must be in [0,1000[") return None, None - + if hour2 < 0 or hour2 > 23: logger.error("hour must be in [0,24[") return None, None @@ -2005,20 +1997,20 @@ def parseTimeInterval(interval): if ms2 < 0 or ms2 > 1000: logger.error("milliseconds must be in [0,1000[") return None, None - + ts1 = timedelta(hours=hour1, minutes=minute1, seconds=second1, microseconds=ms1*1000) ts2 = timedelta(hours=hour2, minutes=minute2, seconds=second2, microseconds=ms2*1000) - + if ts2 < ts1: logger.error("Non monotonic interval") return None,None - + return (ts1, ts2) def compareTimeInterval(interval1, interval2): ts11,ts12 = interval1 ts21,ts22 = interval2 - + if ts12 < ts21: return -1 elif ts22 < ts11: @@ -2028,21 +2020,20 @@ def compareTimeInterval(interval1, interval2): def ffmpegConvert(ffmpeg, ffprobe, inputFile, inputFormat, outputFile, outputFormat, duration): logger = logging.getLogger(__name__) - + width, height = getVideoDimensions(ffprobe, inputFile) subtitles = withSubtitles(ffprobe, inputFile) - + infd = inputFile.fileno() outfd = outputFile.fileno() set_inheritable(infd, True) set_inheritable(outfd, True) - - + if logger.getEffectiveLevel() == logging.DEBUG: log = [] else: log = [ '-loglevel', 'quiet' ] - + params = [ffmpeg, '-y',]+log+['-progress', '/dev/stdout', '-canvas_size', '%dx%d' % (width, height), '-f', inputFormat, '-i', '/proc/self/fd/%d' % infd, '-map', '0:v', '-map', '0:a'] if subtitles: @@ -2051,9 +2042,9 @@ def ffmpegConvert(ffmpeg, ffprobe, inputFile, inputFormat, outputFile, outputFor if subtitles: params.extend(['-scodec', 'dvdsub']) params.extend(['-r:0', '25', '-f', outputFormat, '/proc/self/fd/%d' % outfd]) - - logger.debug('Executing %s' % params) - + + logger.debug('Executing %s', params) + with Popen(params, stdout=PIPE, close_fds=False) as ffmpeg: pb = tqdm(TextIOWrapper(ffmpeg.stdout, encoding="utf-8"), total=int(duration/timedelta(seconds=1)), unit='s', desc='Conversion') for line in pb: @@ -2069,7 +2060,7 @@ def ffmpegConvert(ffmpeg, ffprobe, inputFile, inputFormat, outputFile, outputFor def getTSFrame(frame): logger = logging.getLogger(__name__) - + if 'pts_time' in frame: pts_time = float(frame['pts_time']) elif 'pkt_pts_time' in frame: @@ -2083,7 +2074,7 @@ def getTSFrame(frame): def getPacketDuration(packet): logger = logging.getLogger(__name__) - + if 'duration' in packet: duration = int(packet['duration']) elif 'pkt_duration' in packet: @@ -2091,9 +2082,8 @@ def getPacketDuration(packet): else: logger.error('Impossible to find duration of packet %s', packet) return None - + return duration - def getFramesInStream(ffprobe, inputFile, begin, end, streamKind, subStreamId=0): logger = logging.getLogger(__name__) @@ -2254,7 +2244,7 @@ def getNearestIFrame(ffprobe, inputFile, timestamp, before=True, deltaMax=timede def extractMKVPart(mkvmerge, inputFile, outputFile, begin, end): logger = logging.getLogger(__name__) - + logger.info('Extract video between I-frames at %s and %s' % (begin,end)) infd = inputFile.fileno() outfd = outputFile.fileno() @@ -2264,10 +2254,10 @@ def extractMKVPart(mkvmerge, inputFile, outputFile, begin, end): set_inheritable(outfd, True) env = {**os.environ, 'LANG': 'C'} warnings = [] - + command = [mkvmerge, '-o', '/proc/self/fd/%d' % outfd, '--split', 'parts:%s-%s' % (begin, end), '/proc/self/fd/%d' % infd] logger.debug('Executing: %s' % command) - + with Popen(command, stdout=PIPE, close_fds=False, env=env) as mkvmerge: pb = tqdm(TextIOWrapper(mkvmerge.stdout, encoding="utf-8"), total=100, unit='%', desc='Extraction') for line in pb: @@ -2282,8 +2272,7 @@ def extractMKVPart(mkvmerge, inputFile, outputFile, begin, end): pb.update(100-pb.n) pb.refresh() pb.close() - - + status = mkvmerge.wait() if status == 1: logger.warning('Extraction returns warning') @@ -2292,10 +2281,8 @@ def extractMKVPart(mkvmerge, inputFile, outputFile, begin, end): elif status == 2: logger.error('Extraction returns errors') - def extractPictures(ffmpeg, inputFile, begin, nbFrames, width=640, height=480): logger = logging.getLogger(__name__) - infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) @@ -2308,11 +2295,11 @@ def extractPictures(ffmpeg, inputFile, begin, nbFrames, width=640, height=480): imageLength = width*height*3+headerLen length = imageLength*nbFrames logger.debug("Estimated length: %d" % length) - + command = [ffmpeg, '-loglevel', 'quiet' ,'-y', '-ss', '%s'%begin, '-i', '/proc/self/fd/%d' % infd, '-s', '%dx%d'%(width, height), '-vframes', '%d'%nbFrames, '-c:v', 'ppm', '-f', 'image2pipe', '/proc/self/fd/%d' % outfd ] - logger.debug('Executing: %s' % command) - + logger.debug('Executing: %s', command) + images = bytes() with Popen(command, stdout=PIPE, close_fds=False) as ffmpeg: status = ffmpeg.wait() @@ -2325,13 +2312,13 @@ def extractPictures(ffmpeg, inputFile, begin, nbFrames, width=640, height=480): if len(images) != length: logger.error("Received %d bytes but %d were expected." % (len(images), length)) return None, None - + lseek(outfd, 0, SEEK_SET) return images, outfd def extractSound(ffmpeg, inputFile, begin, outputFileName, packetDuration, subChannel=0, nbPackets=0, sampleRate=48000, nbChannels=2): logger = logging.getLogger(__name__) - + outfd = memfd_create(outputFileName, flags=0) infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) @@ -2339,36 +2326,35 @@ def extractSound(ffmpeg, inputFile, begin, outputFileName, packetDuration, subCh set_inheritable(outfd, True) sound = bytes() length = int(nbChannels*sampleRate*4*nbPackets*packetDuration/1000) - + command = [ffmpeg, '-y', '-loglevel', 'quiet', '-ss', '%s'%begin, '-i', '/proc/self/fd/%d' % infd, '-frames:a:%d' % subChannel, '%d' % (nbPackets+1), '-c:a', 'pcm_s32le', '-sample_rate', '%d' % sampleRate, '-channels', '%d' % nbChannels, '-f', 's32le', '/proc/self/fd/%d' % outfd] - logger.debug('Executing: %s' % command) - + logger.debug('Executing: %s', command) + with Popen(command, stdout=PIPE, close_fds=False) as ffmpeg: status = ffmpeg.wait() if status != 0: logger.error('Sound extraction returns error code: %d' % status) return None, None - + lseek(outfd, 0, SEEK_SET) sound = read(outfd, length) - + if (len(sound) != length): - logger.info("Received %d bytes but %d were expected (channels=%d, freq=%d, packets=%d, duration=%d ms)." % (len(sound), length, nbChannels, sampleRate, nbPackets, packetDuration)) + logger.info("Received %d bytes but %d were expected (channels=%d, freq=%d, packets=%d, duration=%d ms)." % (len(sound), length, nbChannels, sampleRate, nbPackets, packetDuration)) return None, None - + return sound, outfd def dumpPPM(pictures, prefix, temporaries): logger = logging.getLogger(__name__) - # "P6\nWIDTH HEIGHT\n255\n" pos = 0 picture = 0 - + logger.debug('Dumping %d pictures: %s' % (len(pictures),prefix)) - + while pos 0): packetDuration = getPacketDuration(packets[0]) if packetDuration is None: return None else: packetDuration = 0 - - - logger.info("Extracting %d packets of audio stream: a:%d" % (nbPackets, audioID)) + + logger.info("Extracting %d packets of audio stream: a:%d" , nbPackets, audioID) tmpname = '%s-%d.pcm' % (filesPrefix,audioID) - + soundBytes, memfd = extractSound(ffmpeg=ffmpeg, inputFile=inputFile, begin=begin, nbPackets=nbPackets, packetDuration=packetDuration, outputFileName=tmpname, sampleRate=sampleRate, nbChannels=nbChannels) - + if soundBytes is None: exit(-1) - + memfds.append(memfd) - + if dumpMemFD: try: output = open(tmpname,'w') @@ -2515,11 +2499,11 @@ def extractAllStreams(ffmpeg, ffprobe, inputFile, begin, end, streams, filesPref while pos < len(soundBytes): pos+=write(outfd, soundBytes[pos:]) temporaries.append(output) - + # We rewind to zero the memory file descriptor lseek(memfd, 0, SEEK_SET) set_inheritable(memfd, True) - + genericInputParams.extend(['-f', 's32le', '-ar', '%d'%sampleRate, '-ac', '%d'%nbChannels, '-i', '/proc/self/fd/%d' % memfd]) genericCodecParams.extend(['-c:a:%d' % audioID, codec, '-b:a:%d' % audioID, '%d' % bitRate]) audioID=audioID+1 @@ -2537,7 +2521,7 @@ def extractAllStreams(ffmpeg, ffprobe, inputFile, begin, end, streams, filesPref # Create a new MKV movie with all streams (except videos) that have been extracted. genericEncoderParams.extend(genericInputParams) - + for index in range(0,audioID+subTitleID): genericEncoderParams.extend(['-map', '%d' % index]) genericEncoderParams.extend(genericCodecParams) @@ -2548,11 +2532,11 @@ def extractAllStreams(ffmpeg, ffprobe, inputFile, begin, end, streams, filesPref except IOError: logger.error('Impossible to create file: %s' % mkvFileName) return None - + mkvoutfd = mkvOutput.fileno() set_inheritable(mkvoutfd, True) genericEncoderParams.extend(['-f', 'matroska', '/proc/self/fd/%d' % mkvoutfd]) - + logger.info('Encoding all streams (except video) into a MKV file: %s' % mkvFileName) logger.debug('Executing: %s' % genericEncoderParams) with Popen(genericEncoderParams, stdout=PIPE, close_fds=False) as ffmpeg: @@ -2560,27 +2544,26 @@ def extractAllStreams(ffmpeg, ffprobe, inputFile, begin, end, streams, filesPref if status != 0: logger.error('Encoding failed with status code: %d' % status) return None - + temporaries.append(mkvOutput) - + h264FileName = '%s.h264' % filesPrefix try: h264Output = open(h264FileName,'wb+') except IOError: logger.error('Impossible to create file: %s' % h264FileName) return None - + h264outfd = h264Output.fileno() set_inheritable(h264outfd, True) - - + videoEncoderParams.extend(videoInputParams) videoEncoderParams.extend(videoCodecParams) - + videoEncoderParams.extend([ '-x264opts', 'keyint=1:sps-id=%d' % 1,'-bsf:v', 'h264_mp4toannexb,dump_extra=freq=keyframe,h264_metadata=overscan_appropriate_flag=1:sample_aspect_ratio=1:video_format=0:chroma_sample_loc_type=0', '-f', 'h264', '/proc/self/fd/%d' % h264outfd]) - + logger.info('Encoding video into a H264 file: %s' % h264FileName) logger.debug('Executing: %s' % videoEncoderParams) with Popen(videoEncoderParams, stdout=PIPE, close_fds=False) as ffmpeg: @@ -2588,16 +2571,16 @@ def extractAllStreams(ffmpeg, ffprobe, inputFile, begin, end, streams, filesPref if status != 0: logger.error('Encoding failed with status code: %d' % status) return None - + temporaries.append(h264Output) - + h264TSFileName = '%s-ts.txt' % filesPrefix try: h264TSOutput = open(h264TSFileName,'w+') except IOError: - logger.error('Impossible to create file: %s' % h264TSFileName) + logger.error('Impossible to create file: %s', h264TSFileName) return None - + h264TSOutput.write('# timestamp format v2\n') ts = 0 for frame in range(0,nbFrames): @@ -2607,12 +2590,12 @@ def extractAllStreams(ffmpeg, ffprobe, inputFile, begin, end, streams, filesPref h264TSOutput.seek(0) temporaries.append(h264TSOutput) - + for memfd in memfds: close(memfd) - + return h264Output, h264TSOutput, mkvOutput - + else: # Nothing to be done. We are already at a i-frame boundary. return None, None @@ -2620,23 +2603,23 @@ def extractAllStreams(ffmpeg, ffprobe, inputFile, begin, end, streams, filesPref # Merge a list of mkv files passed as input, and produce a new MKV as output def mergeMKVs(mkvmerge, inputs, outputName, concatenate=True, timestamps=None): logger = logging.getLogger(__name__) - + fds = [] try: out = open(outputName, 'w+') except IOError: - logger.error('Impossible to create file: %s' % outputName) + logger.error('Impossible to create file: %s', outputName) return None - + outfd = out.fileno() lseek(outfd, 0, SEEK_SET) fds.append(outfd) set_inheritable(outfd, True) - + # Timestamps of merged tracks are modified by the length of the preceding track. # The default mode ('file') is using the largest timestamp of the whole file which may create desynchronize video and sound. mergeParams = [mkvmerge, '--append-mode', 'track'] - + first = True partNum = 0 for mkv in inputs: @@ -2659,14 +2642,14 @@ def mergeMKVs(mkvmerge, inputs, outputName, concatenate=True, timestamps=None): else: mergeParams.append('/proc/self/fd/%d' % fd) partNum+=1 - + mergeParams.extend(['-o', '/proc/self/fd/%d' % outfd]) - + # We merge all files. warnings = [] env = {**os.environ, 'LANG': 'C'} - logger.debug('Executing: LANG=C %s' % mergeParams) - + logger.debug('Executing: LANG=C %s', mergeParams) + with Popen(mergeParams, stdout=PIPE, close_fds=False, env=env) as mkvmerge: pb = tqdm(TextIOWrapper(mkvmerge.stdout, encoding="utf-8"), total=100, unit='%', desc='Merging') for line in pb: @@ -2679,7 +2662,7 @@ def mergeMKVs(mkvmerge, inputs, outputName, concatenate=True, timestamps=None): pb.update() elif line.startswith('Warning'): warnings.append(line) - + status = mkvmerge.wait() if status == 1: logger.warning('Extraction returns warning') @@ -2687,22 +2670,22 @@ def mergeMKVs(mkvmerge, inputs, outputName, concatenate=True, timestamps=None): logger.warning(w) elif status == 2: logger.error('Extraction returns errors') - + for fd in fds: set_inheritable(fd, False) - + return out def findSubtitlesTracks(ffprobe, inputFile): logger = logging.getLogger(__name__) - + infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) set_inheritable(infd, True) - + command = [ffprobe, '-loglevel','quiet', '-i', '/proc/self/fd/%d' % infd, '-select_streams', 's', '-show_entries', 'stream=index:stream_tags=language', '-of', 'json'] - logger.debug('Executing: %s' % command) - + logger.debug('Executing: %s', command) + with Popen(command, stdout=PIPE, close_fds=False) as ffprobe: out, _ = ffprobe.communicate() out = json.load(BytesIO(out)) @@ -2710,30 +2693,29 @@ def findSubtitlesTracks(ffprobe, inputFile): return out['streams'] else: logger.error('Impossible to retrieve format of file') - + ffprobe.wait() - def extractTrackFromMKV(mkvextract, inputFile, index, outputFile, timestamps): logger = logging.getLogger(__name__) - + infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) set_inheritable(infd, True) - + outfd = outputFile.fileno() lseek(outfd, 0, SEEK_SET) set_inheritable(outfd, True) - + tsfd = timestamps.fileno() lseek(tsfd, 0, SEEK_SET) set_inheritable(tsfd, True) - + params = [ mkvextract, '/proc/self/fd/%d' % infd, 'tracks', '%d:/proc/self/fd/%d' % (index, outfd), 'timestamps_v2', '%d:/proc/self/fd/%d' % (index, tsfd)] - + env = {**os.environ, 'LANG': 'C'} logger.debug('Executing: LANG=C %s' % params) - + with Popen(params, stdout=PIPE, close_fds=False, env=env) as extract: pb = tqdm(TextIOWrapper(extract.stdout, encoding="utf-8"), total=100, unit='%', desc='Extraction of track') for line in pb: @@ -2746,18 +2728,18 @@ def extractTrackFromMKV(mkvextract, inputFile, index, outputFile, timestamps): pb.update(100-pb.n) pb.refresh() pb.close() - + extract.wait() - + if extract.returncode != 0: - logger.error('Mkvextract returns an error code: %d' % extract.returncode) + logger.error('Mkvextract returns an error code: %d', extract.returncode) return None else: - logger.info('Track %d was succesfully extracted.' % index) + logger.info('Track %d was succesfully extracted.', index) def removeVideoTracksFromMKV(mkvmerge, inputFile, outputFile): logger = logging.getLogger(__name__) - + outfd = outputFile.fileno() infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) @@ -2766,8 +2748,8 @@ def removeVideoTracksFromMKV(mkvmerge, inputFile, outputFile): set_inheritable(outfd, True) params = [ mkvmerge, '-o', '/proc/self/fd/%d' % outfd, '-D', '/proc/self/fd/%d' % infd] - logger.debug('Executing: LANG=C %s' % params) - + logger.debug('Executing: LANG=C %s', params) + env = {**os.environ, 'LANG': 'C'} with Popen(params, stdout=PIPE, close_fds=False, env=env) as remove: pb = tqdm(TextIOWrapper(remove.stdout, encoding="utf-8"), total=100, unit='%', desc='Removal of video track:') @@ -2781,38 +2763,38 @@ def removeVideoTracksFromMKV(mkvmerge, inputFile, outputFile): pb.update(100-pb.n) pb.refresh() pb.close() - + remove.wait() - + if remove.returncode != 0: - logger.error('Mkvmerge returns an error code: %d' % remove.returncode) + logger.error('Mkvmerge returns an error code: %d', remove.returncode) return None else: logger.info('Video tracks were succesfully extracted.') def remuxSRTSubtitles(mkvmerge, inputFile, outputFileName, subtitles): logger = logging.getLogger(__name__) - + try: out = open(outputFileName, 'w') except IOError: logger.error('Impossible to create file: %s' % outputFileName) return None - + outfd = out.fileno() infd = inputFile.fileno() lseek(infd, 0, SEEK_SET) set_inheritable(infd, True) set_inheritable(outfd, True) - + mkvmergeParams = [mkvmerge, '/proc/self/fd/%d' % infd] for fd, lang in subtitles: lseek(fd, 0, SEEK_SET) set_inheritable(fd, True) mkvmergeParams.extend(['--language', '0:%s' % lang, '/proc/self/fd/%d' % fd]) - + mkvmergeParams.extend(['-o', '/proc/self/fd/%d' % outfd]) - + warnings = [] env = {**os.environ, 'LANG': 'C'} logger.info('Remux subtitles: %s' % mkvmergeParams) @@ -2828,7 +2810,7 @@ def remuxSRTSubtitles(mkvmerge, inputFile, outputFileName, subtitles): pb.update() elif line.startswith('Warning'): warnings.append(line) - + status = mkvmerge.wait() if status == 1: logger.warning('Remux subtitles returns warning') @@ -2839,17 +2821,17 @@ def remuxSRTSubtitles(mkvmerge, inputFile, outputFileName, subtitles): def concatenateH264Parts(h264parts, output): logger = logging.getLogger(__name__) - + totalLength = 0 for h264 in h264parts: fd = h264.fileno() totalLength += fstat(fd).st_size - - logger.info('Total length: %d' % totalLength) - + + logger.info('Total length: %d', totalLength) + outfd = output.fileno() lseek(outfd, 0, SEEK_SET) - + pb = tqdm(total=totalLength, unit='bytes', desc='Concatenation') for h264 in h264parts: fd = h264.fileno() @@ -2863,13 +2845,13 @@ def concatenateH264Parts(h264parts, output): nbBytes = write(outfd, buf[pos:]) pb.update(nbBytes) pos += nbBytes - + def concatenateH264TSParts(h264TSParts, output): logger = logging.getLogger(__name__) header = '# timestamp format v2\n' - + output.write(header) - + last = 0. first = True for part in h264TSParts: @@ -2893,23 +2875,24 @@ def concatenateH264TSParts(h264TSParts, output): if first: first = False +# TODO: finish this procedure def doCoarseProcessing(ffmpeg, ffprobe, mkvmerge, inputFile, begin, end, nbFrames, frameRate, filesPrefix, streams, width, height, temporaries, dumpMemFD): logger = logging.getLogger(__name__) - + # Internal video with all streams (video, audio and subtitles) internalMKVName = '%s.mkv' % filesPrefix - + try: internalMKV = open(internalMKVName, 'w+') except IOError: logger.error('Impossible to create file: %s', internalMKVName) exit(-1) - + # Extract internal part of MKV extractMKVPart(mkvmerge=mkvmerge, inputFile=inputFile, outputFile=internalMKV, begin=begin, end=end) - + temporaries.append(internalMKV) - + pass def main(): @@ -2926,15 +2909,15 @@ def main(): parser.add_argument("-s","--srt", action='store_true', dest='srt', help="Dump subtitles and make OCR and finally remux them in the movie (as SRT).") parser.add_argument("-v","--verbose", action='store_true', dest='verbose', help="Debug.") parser.add_argument("-f","--framerate", action='store', type=int, help="Override frame rate estimator.") - + args = parser.parse_args() logger.info('Arguments: %s' % args) - + if args.verbose: logger.info('Setting logging to debug mode') coloredlogs.set_level(level=logging.DEBUG) - - logger.debug('Arguments: %s' % args) + + logger.debug('Arguments: %s', args) if args.coarse and args.threshold is not None: logger.error('--coarse and threshold arguments are exclusive.') @@ -2942,7 +2925,7 @@ def main(): if (not args.coarse) and args.threshold is None: args.threshold = 0 - + allOptionalTools, paths = checkRequiredTools() # Flatten args.parts @@ -2960,10 +2943,10 @@ def main(): logger.error("Illegal time interval: %s" % interval) exit(-1) parts.append((ts1,ts2)) - + # Sort intervals parts.sort(key=cmp_to_key(compareTimeInterval)) - + # Check that no intervals are overlapping prevts = timedelta(0) for part in parts: @@ -2985,15 +2968,15 @@ def main(): except IOError: logger.error("Impossible to open %s" % args.inputFile) exit(-1) - + formatOfFile = getFormat(paths['ffprobe'], inputFile) - + if formatOfFile is None: exit(-1) - + duration = timedelta(seconds=float(formatOfFile['duration'])) logger.info("Durée de l'enregistrement: %s" % duration) - + if args.framerate is None: frameRate = getFrameRate(paths['ffprobe'], inputFile) if frameRate is None: @@ -3003,7 +2986,7 @@ def main(): frameRate = args.framerate logger.info('Frame rate: %.1f fps' % frameRate) - + found = False for f in SupportedFormat: if 'format_name' in formatOfFile: @@ -3011,7 +2994,7 @@ def main(): found = True formatOfFile = f break - + if not found: logger.error('Unsupported format of file') @@ -3026,13 +3009,13 @@ def main(): mkv = open(mkvfilename, 'w+') except IOError: logger.error('') - + ffmpegConvert(paths['ffmpeg'], paths['ffprobe'], mp4, 'mp4', mkv, 'matroska', duration) if nbParts > 0: temporaries.append(mkv) except IOError: logger.error('') - + elif formatOfFile == SupportedFormat.MP4: logger.info("Converting MP4 to MKV") try: @@ -3045,9 +3028,9 @@ def main(): else: logger.info("Already in MKV") mkv = inputFile - + streams = getStreams(paths['ffprobe'], mkv) - + logger.debug('Streams: %s' % streams) mainVideo = None nbVideos = 0 @@ -3065,19 +3048,19 @@ def main(): height = stream['height'] else: mainVideo = None - + if mainVideo is None: logger.error('Impossible to find main video stream.') exit(-1) - + # We retrieve the main private codec data _, mainCodecPrivateData = getCodecPrivateDataFromMKV(mkvinfo=paths['mkvinfo'], inputFile=mkv) logger.debug('Main video stream has following private data: %s' % hexdump.dump(mainCodecPrivateData, sep=':')) - + # We parse them mainAvcConfig = parseCodecPrivate(mainCodecPrivateData) logger.debug('AVC configuration: %s' % mainAvcConfig) - + # We check if the parse and dump operations are idempotent. privateData = dumpCodecPrivateData(mainAvcConfig) logger.debug('Redump AVC configuration: %s' % hexdump.dump(privateData, sep=':')) @@ -3091,8 +3074,7 @@ def main(): if isoAvcConfig != mainAvcConfig: logger.error('AVC configurations are different: %s\n%s\n' % (mainAvcConfig, isoAvcConfig)) exit(-1) - - + # Pour chaque portion partnum = 0 mkvparts = [] @@ -3100,10 +3082,9 @@ def main(): h264TS = [] checks = [] pos = timedelta() - - + otherAvcConfigs = [] - + for ts1, ts2 in parts: # Trouver l'estampille de la trame 'I' la plus proche (mais postérieure) au début de la portion. # Trouver l'estampille de la trame 'I' la plus proche (mais antérieure) à la fin de la portion. @@ -3114,26 +3095,26 @@ def main(): # Sinon on extrait les trames 'B' ou 'P' depuis le début jusqu'à la trame 'I' non incluse. # Si la trame de fin précède une trame I, on n'a rien à faire. # Sinon on extrait toutes les trames depuis la dernière trame I jusqu'à la trame de fin. - + partnum = partnum + 1 - + # Get the nearest I-frame whose timestamp is greater or equal to the beginning. headFrames = getNearestIFrame(paths['ffprobe'], mkv, ts1, before=False) if headFrames is None: exit(-1) - + # Get the nearest I-frame whose timestamp ... # TODO: wrong here ... tailFrames = getNearestIFrame(paths['ffprobe'], mkv, ts2, before=True) if tailFrames is None: exit(-1) - + nbHeadFrames, headIFrame = headFrames nbTailFrames, tailIFrame = tailFrames - + logger.info("Found %d frames between beginning of current part and first I-frame", nbHeadFrames) logger.info("Found %d frames between last I-frame and end of current part", nbTailFrames) - + headIFrameTS = getTSFrame(headIFrame) if headIFrameTS is None: exit(-1) @@ -3142,21 +3123,20 @@ def main(): exit(-1) checks.append(pos+headIFrameTS-ts1) - + subparts = [] - + # TODO: separate pipeline processing between coarse and not fine grain options. - + # if args.coarse: # doCoarseProcessing(ffmpeg=paths['ffmpeg'], ffprobe=paths['ffprobe'], inputFile=mkv, begin=ts1, end=headIFrameTS, nbFrames=nbHeadFrames-1, frameRate=frameRate, filesPrefix='part-%d-head' % (partnum), streams=streams, width=width, height=height, temporaries=temporaries, dumpMemFD=args.dump) # else: # doFineGrainProcessing(ffmpeg=paths['ffmpeg'], ffprobe=paths['ffprobe'], inputFile=mkv, begin=ts1, end=headIFrameTS, nbFrames=nbHeadFrames-1, frameRate=frameRate, filesPrefix='part-%d-head' % (partnum), streams=streams, width=width, height=height, temporaries=temporaries, dumpMemFD=args.dump) - - + if (not args.coarse) and (nbHeadFrames > args.threshold): # We extract all frames between the beginning upto the frame that immediately preceeds the I-frame. h264Head, h264HeadTS, mkvHead = extractAllStreams(ffmpeg=paths['ffmpeg'], ffprobe=paths['ffprobe'], inputFile=mkv, begin=ts1, end=headIFrameTS, nbFrames=nbHeadFrames-1, frameRate=frameRate, filesPrefix='part-%d-head' % (partnum), streams=streams, width=width, height=height, temporaries=temporaries, dumpMemFD=args.dump) - + # If we are not at an exact boundary: if mkvHead is not None: subparts.append(mkvHead) @@ -3166,8 +3146,7 @@ def main(): h264parts.append(h264Head) if h264HeadTS is not None: h264TS.append(h264HeadTS) - - + # Creating MKV file that corresponds to current part between I-frames # Internal video with all streams (video, audio and subtitles) internalMKVName = 'part-%d-internal.mkv' % partnum @@ -3177,25 +3156,25 @@ def main(): internalH264TSName = 'part-%d-internal-ts.txt' % partnum # Internal video with only audio and subtitles streams internalNoVideoMKVName = 'part-%d-internal-novideo.mkv' % partnum - + try: internalMKV = open(internalMKVName, 'w+') except IOError: logger.error('Impossible to create file: %s', internalMKVName) exit(-1) - + try: internalNoVideoMKV = open(internalNoVideoMKVName, 'w+') except IOError: logger.error('Impossible to create file: %s', internalNoVideoMKVName) exit(-1) - + try: internalH264 = open(internalH264Name, 'w+') except IOError: logger.error('Impossible to create file: %s', internalH264Name) exit(-1) - + try: internalH264TS = open(internalH264TSName, 'w+') except IOError: @@ -3203,31 +3182,30 @@ def main(): exit(-1) # logger.info('Merge header, middle and trailer subpart into: %s' % internalMKVName) - # Extract internal part of MKV extractMKVPart(mkvmerge=paths['mkvmerge'], inputFile=mkv, outputFile=internalMKV, begin=headIFrameTS, end=tailIFrameTS) - + # Extract video stream of internal part as a raw H264 and its timestamps. logger.info('Extract video track as raw H264 file.') extractTrackFromMKV(mkvextract=paths['mkvextract'], inputFile=internalMKV, index=0, outputFile=internalH264, timestamps=internalH264TS) - + # Remove video track from internal part of MKV logger.info('Remove video track from %s', internalMKVName) removeVideoTracksFromMKV(mkvmerge=paths['mkvmerge'], inputFile=internalMKV, outputFile=internalNoVideoMKV) - + temporaries.append(internalMKV) temporaries.append(internalH264) temporaries.append(internalH264TS) temporaries.append(internalNoVideoMKV) - + h264parts.append(internalH264) h264TS.append(internalH264TS) subparts.append(internalNoVideoMKV) - + if (not args.coarse) and (nbTailFrames > args.threshold): # We extract all frames between the I-frame (including it) upto the end. h264Tail, h264TailTS, mkvTail = extractAllStreams(ffmpeg=paths['ffmpeg'], ffprobe=paths['ffprobe'], inputFile=mkv, begin=tailIFrameTS, end=ts2, nbFrames=nbTailFrames, frameRate=frameRate, filesPrefix='part-%d-tail' % (partnum), streams=streams, width=width, height=height, temporaries=temporaries, dumpMemFD=args.dump) - + if mkvTail is not None: subparts.append(mkvTail) if h264Tail is not None: @@ -3236,22 +3214,22 @@ def main(): h264parts.append(h264Tail) if h264TailTS is not None: h264TS.append(h264TailTS) - + logger.info('Merging MKV: %s', subparts) part = mergeMKVs(mkvmerge=paths['mkvmerge'], inputs=subparts, outputName="part-%d.mkv" % partnum, concatenate=True) mkvparts.append(part) temporaries.append(part) - + pos = pos+tailIFrameTS-ts1 - + # We need to check the end also checks.append(pos) - + # When using coarse option there is a single AVC configuration. for avcConfig in otherAvcConfigs: mainAvcConfig.merge(avcConfig) logger.debug('Merged AVC configuration: %s', mainAvcConfig) - + nbMKVParts = len(mkvparts) if nbMKVParts > 0: try: @@ -3259,24 +3237,24 @@ def main(): except IOError: logger.error('Impossible to create file full H264 stream.') exit(-1) - + logger.info('Merging all H264 tracks') concatenateH264Parts(h264parts=h264parts, output=fullH264) temporaries.append(fullH264) - + try: fullH264TS = open('%s-ts.txt' % basename, 'w+') except IOError: logger.error('Impossible to create file containing all video timestamps.') exit(-1) - + logger.info('Merging H264 timestamps') concatenateH264TSParts(h264TSParts=h264TS, output=fullH264TS) temporaries.append(fullH264TS) - + finalNoVideoName = '%s-novideo.mkv' % basename finalWithVideoName = '%s-video.mkv' % basename - + if nbMKVParts > 1: logger.info('Merging all audio and subtitles parts: %s' % mkvparts) mergeMKVs(mkvmerge=paths['mkvmerge'], inputs=mkvparts, outputName=finalNoVideoName, concatenate=True) @@ -3285,26 +3263,25 @@ def main(): else: logger.info("Nothing else to do.") copyfile(mkvfilename, finalWithVideoName) - + if nbMKVParts >=1 : try: finalNoVideo = open(finalNoVideoName, 'r') except IOError: logger.error('Impossible to open file: %s.' % finalNoVideoName) exit(-1) - + temporaries.append(finalNoVideo) - + fullH264TS.seek(0) - + logger.info('Merging final video track and all other tracks together') finalWithVideo = mergeMKVs(mkvmerge=paths['mkvmerge'], inputs=[fullH264, finalNoVideo], outputName=finalWithVideoName, concatenate=False, timestamps={0: fullH264TS}) finalCodecPrivateData = dumpCodecPrivateData(mainAvcConfig) logger.debug('Final codec private data: %s' % hexdump.dump(finalCodecPrivateData, sep=':')) logger.info('Changing codec private data with the new one.') changeCodecPrivateData(paths['mkvinfo'], finalWithVideo, finalCodecPrivateData) - - + if args.srt: if not allOptionalTools: logger.warning("Missing tools for extracting subtitles.") @@ -3332,7 +3309,7 @@ def main(): logger.error("Dropping subtitle: %s because it is missing language indication") else: logger.error("Dropping subtitle: %s because it is missing language indication") - + logger.info(sts) if len(sts) > 0: listOfSubtitles = extractSRT(paths['mkvextract'], finalWithVideoName, sts, supportedLangs) @@ -3348,20 +3325,20 @@ def main(): except IOError: logger.error("Impossible to open %s.", subName) exit(-1) - + temporaries.append(idx) temporaries.append(sub) - + ocr = doOCR(paths['vobsubocr'], listOfSubtitles, duration, temporaries, args.dump) logger.info(ocr) - + # Remux SRT subtitles remuxSRTSubtitles(paths['mkvmerge'], finalWithVideo, args.outputFile, ocr) else: copyfile(finalWithVideoName, args.outputFile) else: move(finalWithVideoName, args.outputFile) - + if not args.keep: logger.info("Cleaning temporary files") for f in temporaries: @@ -3373,6 +3350,6 @@ def main(): d = datetime(1,1,1) for c in checks: logger.info("Please check cut smoothness at %s" % (c+d).strftime("%H:%M:%S")) - + if __name__ == "__main__": main()