├── requirements.dev.txt
├── biim
├── __init__.py
├── mpeg2ts
│ ├── h264.py
│ ├── h265.py
│ ├── pat.py
│ ├── section.py
│ ├── pmt.py
│ ├── ts.py
│ ├── packetize.py
│ ├── pes.py
│ ├── parser.py
│ └── scte.py
├── mp4
│ ├── mp4a.py
│ ├── avc.py
│ ├── box.py
│ └── hevc.py
├── id3
│ ├── priv.py
│ └── txxx.py
├── util
│ ├── reader.py
│ ├── bitstream.py
│ └── bytestream.py
├── rtmp
│ ├── demuxer.py
│ ├── remuxer.py
│ ├── amf0.py
│ └── rtmp.py
├── variant
│ ├── codec.py
│ ├── mpegts.py
│ ├── fmp4.py
│ └── handler.py
└── hls
│ ├── segment.py
│ └── m3u8.py
├── requirements.txt
├── .vscode
└── settings.json
├── .editorconfig
├── .gitignore
├── pyproject.toml
├── LICENSE
├── rtmp.py
├── README.md
├── pseudo.html
├── main.py
├── fmp4.py
├── multi.py
├── pseudo.py
└── pseudo_quality.py
/requirements.dev.txt:
--------------------------------------------------------------------------------
1 | hatch>=1.6.3
2 |
--------------------------------------------------------------------------------
/biim/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = '1.11.0-post1'
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp>=3.9.1
2 | aiohttp-sse>=2.2.0
3 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.analysis.typeCheckingMode": "basic"
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | insert_final_newline = true
3 | trim_trailing_whitespace = true
4 |
5 | [*.py]
6 | indent_size = 2
7 | indent_style = space
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .envrc
2 | .tool-versions
3 | .python-version
4 |
5 | .venv
6 |
7 | __pycache__
8 | *.py[cod]
9 | *$py.class
10 |
11 | dist/
12 |
--------------------------------------------------------------------------------
/biim/mpeg2ts/h264.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import re
4 | from typing import Iterator
5 |
6 | from biim.mpeg2ts.pes import PES
7 |
8 | SPLIT = re.compile('\0\0\0?\1'.encode('ascii'))
9 |
10 | class H264PES(PES):
11 | def __init__(self, payload: bytes | bytearray | memoryview = b''):
12 | super().__init__(payload)
13 | PES_packet_data = self.PES_packet_data()
14 | self.ebsps: list[bytes] = [x for x in re.split(SPLIT, PES_packet_data) if len(x) > 0]
15 |
16 | def __iter__(self) -> Iterator[bytes]:
17 | return iter(self.ebsps)
18 |
--------------------------------------------------------------------------------
/biim/mpeg2ts/h265.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import re
4 | from typing import Iterator
5 |
6 | from biim.mpeg2ts.pes import PES
7 |
8 | SPLIT = re.compile('\0\0\0?\1'.encode('ascii'))
9 |
10 | class H265PES(PES):
11 | def __init__(self, payload: bytes | bytearray | memoryview = b''):
12 | super().__init__(payload)
13 | PES_packet_data = self.PES_packet_data()
14 | self.ebsps: list[bytes] = [x for x in re.split(SPLIT, PES_packet_data) if len(x) > 0]
15 |
16 | def __iter__(self) -> Iterator[bytes]:
17 | return iter(self.ebsps)
18 |
--------------------------------------------------------------------------------
/biim/mp4/mp4a.py:
--------------------------------------------------------------------------------
1 | from biim.mp4.box import trak, tkhd, mdia, mdhd, hdlr, minf, smhd, dinf, stbl, stsd, mp4a
2 |
3 | def mp4aTrack(trackId: int, timescale: int, config: bytes | bytearray | memoryview, channelCount: int, sampleRate: int) -> bytes:
4 | return trak(
5 | tkhd(trackId, 0, 0),
6 | mdia(
7 | mdhd(timescale),
8 | hdlr('soun', 'soundHandler'),
9 | minf(
10 | smhd(),
11 | dinf(),
12 | stbl(
13 | stsd(
14 | mp4a(config, channelCount, sampleRate)
15 | )
16 | )
17 | )
18 | )
19 | )
20 |
--------------------------------------------------------------------------------
/biim/mpeg2ts/pat.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from typing import Iterator
4 |
5 | from biim.mpeg2ts.section import Section
6 |
7 | class PATSection(Section):
8 | def __init__(self, payload: bytes | bytearray | memoryview = b''):
9 | super().__init__(payload)
10 | self.entry: list[tuple[int, int]] = [
11 | ((payload[offset + 0] << 8) | payload[offset + 1], ((payload[offset + 2] & 0x1F) << 8) | payload[offset + 3])
12 | for offset in range(Section.EXTENDED_HEADER_SIZE, 3 + self.section_length() - Section.CRC_SIZE, 4)
13 | ]
14 |
15 | def __iter__(self) -> Iterator[tuple[int, int]]:
16 | return iter(self.entry)
17 |
--------------------------------------------------------------------------------
/biim/id3/priv.py:
--------------------------------------------------------------------------------
1 | def PRIV(owner: str, data: bytes | bytearray | memoryview) -> bytes:
2 | priv_payload = owner.encode('utf-8') + b'\x00' + data
3 | priv_paylaod_size = bytes([
4 | ((len(priv_payload) & 0xFE00000) >> 21),
5 | ((len(priv_payload) & 0x01FC000) >> 14),
6 | ((len(priv_payload) & 0x0003F80) >> 7),
7 | ((len(priv_payload) & 0x000007F) >> 0),
8 | ])
9 | priv_frame = b''.join([
10 | ('PRIV').encode('utf-8'),
11 | priv_paylaod_size,
12 | (0).to_bytes(2, byteorder='big'),
13 | priv_payload,
14 | ])
15 | priv_frame_size = bytes([
16 | ((len(priv_frame) & 0xFE00000) >> 21),
17 | ((len(priv_frame) & 0x01FC000) >> 14),
18 | ((len(priv_frame) & 0x0003F80) >> 7),
19 | ((len(priv_frame) & 0x000007F) >> 0),
20 | ])
21 | return b''.join([
22 | bytes([0x49, 0x44, 0x33, 0x04, 0x00, 0x00]),
23 | priv_frame_size,
24 | priv_frame,
25 | ])
--------------------------------------------------------------------------------
/biim/util/reader.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | class BufferingAsyncReader:
4 | def __init__(self, reader, size: int):
5 | self.reader = reader
6 | self.buffer: bytearray = bytearray()
7 | self.size: int = size
8 |
9 | async def __fill(self) -> bool:
10 | data = await asyncio.to_thread(lambda: self.reader.read(self.size))
11 | if data == b'': return False
12 | self.buffer += data
13 | return True
14 |
15 | async def read(self, n: int) -> memoryview:
16 | while len(self.buffer) < n:
17 | if not (await self.__fill()): break
18 | result = self.buffer[:n]
19 | self.buffer = self.buffer[n:]
20 | return memoryview(result)
21 |
22 | async def readexactly(self, n: int) -> memoryview:
23 | while len(self.buffer) < n:
24 | if not (await self.__fill()): raise asyncio.IncompleteReadError(self.buffer, None)
25 | result = self.buffer[:n]
26 | self.buffer = self.buffer[n:]
27 | return memoryview(result)
28 |
--------------------------------------------------------------------------------
/biim/id3/txxx.py:
--------------------------------------------------------------------------------
1 | def TXXX(description: str, text: str) -> bytes:
2 | txxx_payload = b''.join([
3 | b'\x03', # utf-8
4 | description.encode('utf-8'),
5 | b'\x00',
6 | text.encode('utf-8'),
7 | b'\x00'
8 | ])
9 |
10 | txxx_paylaod_size = bytes([
11 | ((len(txxx_payload) & 0xFE00000) >> 21),
12 | ((len(txxx_payload) & 0x01FC000) >> 14),
13 | ((len(txxx_payload) & 0x0003F80) >> 7),
14 | ((len(txxx_payload) & 0x000007F) >> 0),
15 | ])
16 | txxx_frame = b''.join([
17 | ('TXXX').encode('utf-8'),
18 | txxx_paylaod_size,
19 | (0).to_bytes(2, byteorder='big'),
20 | txxx_payload,
21 | ])
22 | txxx_frame_size = bytes([
23 | ((len(txxx_frame) & 0xFE00000) >> 21),
24 | ((len(txxx_frame) & 0x01FC000) >> 14),
25 | ((len(txxx_frame) & 0x0003F80) >> 7),
26 | ((len(txxx_frame) & 0x000007F) >> 0),
27 | ])
28 | return b''.join([
29 | bytes([0x49, 0x44, 0x33, 0x04, 0x00, 0x00]),
30 | txxx_frame_size,
31 | txxx_frame,
32 | ])
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "biim"
7 | description = 'LL-HLS implementation written in Python3'
8 | url = "https://github.com/monyone/biim"
9 | readme = "README.md"
10 | requires-python = ">=3.10"
11 | license = "MIT"
12 | keywords = []
13 | authors = [
14 | { name = "monyone" },
15 | ]
16 | classifiers = [
17 | "Development Status :: 4 - Beta",
18 | "Programming Language :: Python",
19 | "Programming Language :: Python :: 3.10",
20 | "Programming Language :: Python :: 3.11",
21 | "Programming Language :: Python :: 3.12",
22 | "Programming Language :: Python :: Implementation :: CPython",
23 | "Programming Language :: Python :: Implementation :: PyPy",
24 | ]
25 | dependencies = [
26 | "aiohttp>=3.9.1",
27 | ]
28 | dynamic = ["version"]
29 |
30 | [project.urls]
31 | Documentation = "https://github.com/monyone/biim#readme"
32 | Issues = "https://github.com/monyone/biim/issues"
33 | Source = "https://github.com/monyone/biim"
34 |
35 | [tool.hatch.version]
36 | path = "biim/__init__.py"
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 もにょ~ん
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/biim/mpeg2ts/section.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from typing import Any
4 |
5 | class Section:
6 | BASIC_HEADER_SIZE = 3
7 | EXTENDED_HEADER_SIZE = 8
8 | CRC_SIZE = 4
9 |
10 | def __init__(self, payload: bytes | bytearray | memoryview = b''):
11 | self.payload = memoryview(payload)
12 |
13 | def __getitem__(self, item: Any) -> Any:
14 | return self.payload[item]
15 |
16 | def __len__(self) -> int:
17 | return len(self.payload)
18 |
19 | def table_id(self) -> int:
20 | return self.payload[0]
21 |
22 | def section_length(self) -> int:
23 | return ((self.payload[1] & 0x0F) << 8) | self.payload[2]
24 |
25 | def table_id_extension(self) -> int:
26 | return (self.payload[3] << 8) | self.payload[4]
27 |
28 | def version_number(self) -> int:
29 | return (self.payload[5] & 0x3E) >> 1
30 |
31 | def current_next_indicator(self) -> bool:
32 | return (self.payload[5] & 0x01) != 0
33 |
34 | def section_number(self) -> int:
35 | return self.payload[6]
36 |
37 | def last_section_number(self) -> int:
38 | return self.payload[7]
39 |
40 | def CRC32(self) -> int:
41 | crc = 0xFFFFFFFF
42 | for byte in self.payload:
43 | for index in range(7, -1, -1):
44 | bit = (byte & (1 << index)) >> index
45 | c = 1 if crc & 0x80000000 else 0
46 | crc <<= 1
47 | if c ^ bit: crc ^= 0x04c11db7
48 | crc &= 0xFFFFFFFF
49 | return crc
50 |
--------------------------------------------------------------------------------
/biim/mpeg2ts/pmt.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from typing import Iterator
4 |
5 | from biim.mpeg2ts.section import Section
6 |
7 | class PMTSection(Section):
8 | def __init__(self, payload: bytes | bytearray | memoryview = b''):
9 | super().__init__(payload)
10 | self.PCR_PID: int = ((self.payload[Section.EXTENDED_HEADER_SIZE + 0] & 0x1F) << 8) | self.payload[Section.EXTENDED_HEADER_SIZE + 1]
11 | self.entry: list[tuple[int, int, list[tuple[int, bytes | bytearray | memoryview]]]] = []
12 |
13 | program_info_length: int = ((self.payload[Section.EXTENDED_HEADER_SIZE + 2] & 0x0F) << 8) | self.payload[Section.EXTENDED_HEADER_SIZE + 3]
14 | begin: int = Section.EXTENDED_HEADER_SIZE + 4 + program_info_length
15 | while begin < 3 + self.section_length() - Section.CRC_SIZE:
16 | stream_type = self.payload[begin + 0]
17 | elementary_PID = ((self.payload[begin + 1] & 0x1F) << 8) | self.payload[begin + 2]
18 | ES_info_length = ((self.payload[begin + 3] & 0x0F) << 8) | self.payload[begin + 4]
19 |
20 | descriptors: list[tuple[int, bytes | bytearray | memoryview]] = []
21 | offset = begin + 5
22 | while offset < begin + 5 + ES_info_length:
23 | descriptor_tag = self.payload[offset + 0]
24 | descriptor_length = self.payload[offset + 1]
25 | descriptors.append((descriptor_tag, self.payload[offset + 2: offset + 2 + descriptor_length]))
26 | offset += 2 + descriptor_length
27 |
28 | self.entry.append((stream_type, elementary_PID, descriptors))
29 | begin += 5 + ES_info_length
30 |
31 | def __iter__(self) -> Iterator[tuple[int, int, list[tuple[int, bytes | bytearray | memoryview]]]]:
32 | return iter(self.entry)
33 |
--------------------------------------------------------------------------------
/biim/rtmp/demuxer.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from biim.rtmp.rtmp import Message
4 | from biim.util.bytestream import ByteStream
5 |
6 | # TODO!
7 | class FLVDemuxer(ABC):
8 | def __init__(self):
9 | pass
10 |
11 | def parseRTMP(self, message: Message):
12 | try:
13 | match message.message_type_id:
14 | case 0x08: pass # Audio
15 | case 0x09: self.parseVideoData(message.timestamp, ByteStream(message.chunk)) # Video
16 | case 0x12: pass # Data (AMF0)
17 | except EOFError:
18 | return
19 |
20 | def parseVideoData(self, timestamp: int, stream: ByteStream):
21 | spec = stream.readU8()
22 | is_exheader = (spec & 0b10000000) != 0
23 | if is_exheader:
24 | pass
25 | else:
26 | frame_type = (spec & 0b11110000) >> 4
27 | codec_id = spec & 0b00001111
28 | match codec_id:
29 | case 7: self.parseLegacyAVCVideoPacket(timestamp, frame_type, stream)
30 |
31 | def parseLegacyAVCVideoPacket(self, timestamp: int, frame_type: int, stream: ByteStream):
32 | packet_type = stream.readU8()
33 | cto = stream.readS24()
34 | match packet_type:
35 | case 0: self.onAVCDecoderConfigurationRecord(timestamp, None, stream.readAll()) # AVCDecoderConfigurationRecord
36 | case 1: self.onAVCVideoData(timestamp, None, frame_type, cto, stream.readAll()) # AVCVideoData
37 | case 2: self.onAVCEndOfSequence(timestamp, None) # End of Sequence
38 | pass
39 |
40 | @abstractmethod
41 | def onAVCDecoderConfigurationRecord(self, timestamp: int, track_id: int | None, data: memoryview):
42 | pass
43 |
44 | @abstractmethod
45 | def onAVCVideoData(self, timestamp: int, track_id: int | None, frame_type: int, cto: int, data: memoryview):
46 | pass
47 |
48 | @abstractmethod
49 | def onAVCEndOfSequence(self, timestamp: int, track_id: int | None):
50 | pass
51 |
52 |
--------------------------------------------------------------------------------
/rtmp.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from typing import cast
4 |
5 | import asyncio
6 | import io
7 | from aiohttp import web
8 |
9 | import argparse
10 | from threading import Semaphore
11 |
12 | from biim.rtmp.rtmp import recieve, STREAM_TYPE_ID_AUDIO, STREAM_TYPE_ID_VIDEO, STREAM_TYPE_ID_DATA
13 | from biim.rtmp.amf0 import deserialize
14 | from biim.rtmp.remuxer import FLVfMP4Remuxer
15 |
16 | from biim.hls.m3u8 import M3U8
17 |
18 | async def serve(args):
19 | appName: str = args.app_name
20 | streamKey: str = args.stream_key
21 | connections: int = args.connections
22 |
23 | # Setup Concurrency
24 | semaphore = Semaphore(connections) # Never Blocking, only Limiting
25 |
26 | # Setup RTMP/FLV Reciever
27 | async def connection(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
28 | if not semaphore.acquire(blocking=False): # Exceeded Connectin Capacity
29 | writer.close()
30 | await writer.wait_closed()
31 | return
32 |
33 | # FLV Demuxer definision
34 | class Remuxer(FLVfMP4Remuxer):
35 | pass
36 | remuxer = Remuxer()
37 |
38 | async for message in recieve(reader, writer, appName, streamKey):
39 | remuxer.parseRTMP(message)
40 |
41 | writer.close()
42 | await writer.wait_closed()
43 | semaphore.release()
44 | return connection
45 |
46 | async def main():
47 | parser = argparse.ArgumentParser(description=('biim: LL-HLS origin'))
48 |
49 | parser.add_argument('--window_size', type=int, nargs='?')
50 | parser.add_argument('--target_duration', type=int, nargs='?', default=1)
51 | parser.add_argument('--part_duration', type=float, nargs='?', default=0.25)
52 | parser.add_argument('--hls_port', type=int, nargs='?', default=8080)
53 | parser.add_argument('--rtmp_port', type=int, nargs='?', default=1935)
54 | parser.add_argument('--app_name', type=str, required=True)
55 | parser.add_argument('--stream_key', type=str, required=True)
56 | parser.add_argument('--connections', type=int, default=1)
57 |
58 | args = parser.parse_args()
59 |
60 | server = await asyncio.start_server(await serve(args), 'localhost', args.rtmp_port)
61 | async with server: await server.serve_forever()
62 |
63 | if __name__ == '__main__':
64 | asyncio.run(main())
65 |
--------------------------------------------------------------------------------
/biim/util/bitstream.py:
--------------------------------------------------------------------------------
1 | from collections import deque
2 |
3 | class BitStream:
4 |
5 | def __init__(self, data):
6 | self.bits: deque[int] = deque()
7 | self.data: deque[int] = deque(data)
8 |
9 | def __bool__(self) -> bool:
10 | return bool(self.bits or self.data)
11 |
12 | def __len__(self) -> int:
13 | return len(self.data) * 8 + len(self.bits)
14 |
15 | def __fill_bits(self) -> None:
16 | if not self.data:
17 | return
18 | byte = self.data.popleft()
19 | for index in range(8):
20 | bit_index = (8 - 1) - index
21 | self.bits.append(1 if (byte & (1 << bit_index)) != 0 else 0)
22 |
23 | def __peekBit(self) -> int:
24 | if not self.bits:
25 | self.__fill_bits()
26 | return self.bits[0]
27 |
28 | def __count_trailing_zeros(self) -> int:
29 | result = 0
30 | while self.__peekBit() == 0:
31 | self.readBits(1)
32 | result += 1
33 | return result
34 |
35 | def readBits(self, size: int) -> int:
36 | result = 0
37 | remain_bits_len = min(len(self.bits), size)
38 | for _ in range(remain_bits_len):
39 | result *= 2
40 | result += self.bits.popleft()
41 | size -= 1
42 |
43 | while size >= 8 and self.data:
44 | byte = self.data.popleft()
45 | result *= 256
46 | result += byte
47 | size -= 8
48 | if size == 0:
49 | return result
50 |
51 | self.__fill_bits()
52 | remain_bits_len = min(len(self.bits), size)
53 | for _ in range(remain_bits_len):
54 | result *= 2
55 | result += self.bits.popleft()
56 | size -= 1
57 | return result
58 |
59 | def readBool(self) -> bool:
60 | return self.readBits(1) == 1
61 |
62 | def readByte(self, size: int = 1) -> int:
63 | return self.readBits(size * 8)
64 |
65 | def readBitStreamFromBytes(self, size: int) -> 'BitStream':
66 | return BitStream(bytes([
67 | self.readByte(1) for _ in range(size)
68 | ]))
69 |
70 | def readUEG(self) -> int:
71 | count = self.__count_trailing_zeros()
72 | return self.readBits(count + 1) - 1
73 |
74 | def readSEG(self) -> int:
75 | ueg = self.readUEG()
76 | if ueg % 2 == 1:
77 | return (ueg + 1) >> 1
78 | else:
79 | return -1 * (ueg >> 1)
80 |
81 | def retainByte(self, byte: int) -> None:
82 | for i in range(8):
83 | self.bits.appendleft(1 if byte & (1 << i) != 0 else 0)
84 |
--------------------------------------------------------------------------------
/biim/mpeg2ts/ts.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | PACKET_SIZE = 188
4 | HEADER_SIZE = 4
5 | SYNC_BYTE = b'\x47'
6 | STUFFING_BYTE = b'\xff'
7 | PCR_CYCLE = 2 ** 33
8 | HZ = 90000
9 |
10 | def transport_error_indicator(packet: bytes | bytearray | memoryview):
11 | return (packet[1] & 0x80) != 0
12 |
13 | def payload_unit_start_indicator(packet: bytes | bytearray | memoryview) -> bool:
14 | return (packet[1] & 0x40) != 0
15 |
16 | def transport_priority(packet: bytes | bytearray | memoryview) -> bool:
17 | return (packet[1] & 0x20) != 0
18 |
19 | def pid(packet: bytes | bytearray | memoryview) -> int:
20 | return ((packet[1] & 0x1F) << 8) | packet[2]
21 |
22 | def transport_scrambling_control(packet: bytes | bytearray | memoryview) -> int:
23 | return (packet[3] & 0xC0) >> 6
24 |
25 | def has_adaptation_field(packet: bytes | bytearray | memoryview) -> bool:
26 | return (packet[3] & 0x20) != 0
27 |
28 | def has_payload(packet: bytes | bytearray | memoryview) -> bool:
29 | return (packet[3] & 0x10) != 0
30 |
31 | def continuity_counter(packet: bytes | bytearray | memoryview) -> int:
32 | return packet[3] & 0x0F
33 |
34 | def adaptation_field_length(packet: bytes | bytearray | memoryview) -> int:
35 | return packet[4] if has_adaptation_field(packet) else 0
36 |
37 | def pointer_field(packet: bytes | bytearray | memoryview) -> int:
38 | return packet[HEADER_SIZE + (1 + adaptation_field_length(packet) if has_adaptation_field(packet) else 0)]
39 |
40 | def payload(packet: bytes | bytearray | memoryview) -> bytes | bytearray | memoryview:
41 | return packet[HEADER_SIZE + (1 + adaptation_field_length(packet) if has_adaptation_field(packet) else 0):]
42 |
43 | def has_pcr(packet: bytes | bytearray | memoryview) -> bool:
44 | return has_adaptation_field(packet) and adaptation_field_length(packet) > 0 and (packet[HEADER_SIZE + 1] & 0x10) != 0
45 |
46 | def pcr(packet: bytes | bytearray | memoryview) -> int | None:
47 | if not has_pcr(packet): return None
48 |
49 | pcr_base = 0
50 | pcr_base = (pcr_base << 8) | ((packet[HEADER_SIZE + 1 + 1] & 0xFF) >> 0)
51 | pcr_base = (pcr_base << 8) | ((packet[HEADER_SIZE + 1 + 2] & 0xFF) >> 0)
52 | pcr_base = (pcr_base << 8) | ((packet[HEADER_SIZE + 1 + 3] & 0xFF) >> 0)
53 | pcr_base = (pcr_base << 8) | ((packet[HEADER_SIZE + 1 + 4] & 0xFF) >> 0)
54 | pcr_base = (pcr_base << 1) | ((packet[HEADER_SIZE + 1 + 5] & 0x80) >> 7)
55 | return pcr_base
56 |
--------------------------------------------------------------------------------
/biim/mpeg2ts/packetize.py:
--------------------------------------------------------------------------------
1 | from typing import Type
2 |
3 | from biim.mpeg2ts import ts
4 | from biim.mpeg2ts.section import Section
5 | from biim.mpeg2ts.pes import PES
6 |
7 | def packetize_section(section: Section, transport_error_indicator: bool, transport_priority: bool, pid: int, transport_scrambling_control: int, continuity_counter: int) -> list[bytes]:
8 | result: list[bytes] = []
9 | begin = 0
10 | while (begin < len(section)):
11 | next = min(len(section), begin + (ts.PACKET_SIZE - ts.HEADER_SIZE) - (1 if begin == 0 else 0))
12 | result.append(bytes(
13 | ([
14 | ts.SYNC_BYTE[0],
15 | ((1 if transport_error_indicator else 0) << 7) | ((1 if begin == 0 else 0) << 6) | ((1 if transport_priority else 0) << 5) | ((pid & 0x1F00) >> 8),
16 | (pid & 0x00FF),
17 | (transport_scrambling_control << 6) | (1 << 4) | (continuity_counter & 0x0F),
18 | ]) +
19 | ([0] if begin == 0 else []) +
20 | list(section[begin:next]) +
21 | ([ts.STUFFING_BYTE[0]] * ((ts.PACKET_SIZE - ts.HEADER_SIZE) - ((next - begin) + (1 if begin == 0 else 0))))
22 | ))
23 | continuity_counter = (continuity_counter + 1) & 0x0F
24 | begin = next
25 | return result
26 |
27 | def packetize_pes(pes: PES, transport_error_indicator: bool, transport_priority: bool, pid: int, transport_scrambling_control: int, continuity_counter: int) -> list[bytes]:
28 | result: list[bytes] = []
29 | begin = 0
30 | while (begin < len(pes)):
31 | next = min(len(pes), begin + (ts.PACKET_SIZE - ts.HEADER_SIZE))
32 | packet = bytearray()
33 | packet += bytes([
34 | ts.SYNC_BYTE[0],
35 | ((1 if transport_error_indicator else 0) << 7) | ((1 if begin == 0 else 0) << 6) | ((1 if transport_priority else 0) << 5) | ((pid & 0x1F00) >> 8),
36 | (pid & 0x00FF),
37 | (transport_scrambling_control << 6) | (0x30 if (ts.PACKET_SIZE - ts.HEADER_SIZE) > (next - begin) else 0x10) | (continuity_counter & 0x0F),
38 | ])
39 | if (((ts.PACKET_SIZE - ts.HEADER_SIZE) > (next - begin))):
40 | packet += bytes([((ts.PACKET_SIZE - ts.HEADER_SIZE) - (next - begin)) - 1])
41 | if (((ts.PACKET_SIZE - ts.HEADER_SIZE) > (next - begin + 1))):
42 | packet += b'\x00'
43 | if (((ts.PACKET_SIZE - ts.HEADER_SIZE) > (next - begin + 2))):
44 | packet += bytes([0xFF] * (((ts.PACKET_SIZE - ts.HEADER_SIZE) - (next - begin)) - 2))
45 | packet += bytes(pes[begin:next])
46 | result.append(bytes(packet))
47 | continuity_counter = (continuity_counter + 1) & 0x0F
48 | begin = next
49 | return result
50 |
--------------------------------------------------------------------------------
/biim/variant/codec.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from biim.util.bitstream import BitStream
4 |
5 | escapes = set([0x00, 0x01, 0x02, 0x03])
6 | def ebsp2rbsp(data: bytes | bytearray | memoryview) -> bytes:
7 | rbsp = bytearray(data[:2])
8 | length = len(data)
9 | for index in range(2, length):
10 | if index < length - 1 and data[index - 2] == 0x00 and data[index - 1] == 0x00 and data[index + 0] == 0x03 and data[index + 1] in escapes:
11 | continue
12 | rbsp.append(data[index])
13 | return bytes(rbsp)
14 |
15 | def aac_codec_parameter_string(audioObjectType: int):
16 | return f'mp4a.40.{audioObjectType}'
17 |
18 | def avc_codec_parameter_string(sps: bytes | bytearray | memoryview):
19 | profile_idc = sps[1]
20 | constraint_flags = sps[2]
21 | level_idc = sps[3]
22 | return f'avc1.{profile_idc:02x}{constraint_flags:02x}{level_idc:02x}'
23 |
24 | def hevc_codec_parameter_string(sps: bytes | bytearray | memoryview):
25 | stream = BitStream(ebsp2rbsp(sps))
26 | stream.readByte(2) # remove header
27 | stream.readByte() # video_paramter_set_id, max_sub_layers_minus1, temporal_id_nesting_flag
28 |
29 | general_profile_space = ['', 'A', 'B', 'C'][stream.readBits(2)]
30 | general_tier_flag = 'H' if stream.readBool() else 'L'
31 | general_profile_idc = stream.readBits(5)
32 | general_profile_compatibility_flags = stream.readByte(4)
33 | general_profile_compatibility = 0
34 | for i in range(32): general_profile_compatibility |= ((general_profile_compatibility_flags >> i) & 1) << (31 - i)
35 | general_constraint_indicator_flags = stream.readByte(6).to_bytes(6, byteorder='big')
36 | general_level_idc = stream.readByte()
37 |
38 | codec_parameter_string = f'hvc1.{general_profile_space}{general_profile_idc}.{general_profile_compatibility:X}.{general_tier_flag}{general_level_idc}'
39 | if general_constraint_indicator_flags[5] != 0: codec_parameter_string += f'.{general_constraint_indicator_flags[5]:X}'
40 | if general_constraint_indicator_flags[4] != 0: codec_parameter_string += f'.{general_constraint_indicator_flags[4]:X}'
41 | if general_constraint_indicator_flags[3] != 0: codec_parameter_string += f'.{general_constraint_indicator_flags[3]:X}'
42 | if general_constraint_indicator_flags[2] != 0: codec_parameter_string += f'.{general_constraint_indicator_flags[2]:X}'
43 | if general_constraint_indicator_flags[1] != 0: codec_parameter_string += f'.{general_constraint_indicator_flags[1]:X}'
44 | if general_constraint_indicator_flags[0] != 0: codec_parameter_string += f'.{general_constraint_indicator_flags[0]:X}'
45 |
46 | return codec_parameter_string
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # biim
2 |
3 | An example for Apple Low Lantency HLS Packager and Origin
4 |
5 | ## Feature
6 | * MPEG-TS Demuxing and Apple LL-HLS Packaging in pure Python3 (using asyncio)
7 | * Packaging MPEG-TS stream to HLS media segment (MPEG-TS or fmp4)
8 | * `main.py`: Packaging MPEG-TS stream to MPEG-TS segment (for H.264/AVC, AAC, ID3)
9 | * Support TIMED-ID3 Metadata PassThrough
10 | * `fmp4.py`: Packaging MPEG-TS stream to fmp4 segment (for H.265/HEVC, AAC, ID3)
11 | * Support TIMED-ID3 Metadata to EMSG-ID3 Conversion
12 | * Support LL-HLS Feature (1s Latency with HTTP/2, 2s Latency with HTTP/1.1)
13 | * Support Blocking Request
14 | * Support EXT-X-PRELOAD-HINT with Chunked Transfer
15 | * NOTE: HTTP/2 is currently not Supported. If use with HTTP/2, please proxing HTTP/2 to HTTP/1.1.
16 | * In Memory (On the fly) LL-HLS Serving
17 | * Not use disk space for LL-HLS Delivery
18 |
19 | ## Dependency
20 |
21 | * aiohttp
22 |
23 | ## Usege
24 |
25 | Ingest MPEG-TS Stream to biim's STDIN!
26 |
27 | ```bash
28 | # mpegts (for H.264)
29 | ffmpeg xxx -f mpegts - | ./main.py --port 8080
30 | # fmp4 (for H.264/H.265)
31 | ffmpeg xxx -f mpegts - | ./fmp4.py --port 8080
32 |
33 | # watch http://localhost:8080/playlist.m3u8
34 | ```
35 |
36 | ### Options
37 |
38 | * `-i`, `--input`,
39 | * Specify input source.
40 | * if not Specified, use STDIN.
41 | * if Specified file, throttled for pseudo live serving.
42 | * DEFAULT: STDIN
43 | * `-t`, `--target_duration`
44 | * Specify minmum TARGETDURATION for LL-HLS
45 | * DEFAULT: 1
46 | * `-p`, `--part_duration`
47 | * Specify PART-TARGET for LL-HLS
48 | * DEFAULT: 0.1
49 | * `-w`, `--window_size`
50 | * Specify Live Window for LL-HLS
51 | * if Not Specifed, window size is Infinify, for EVENT(DVR).
52 | * DEFAULT: Infinity (None)
53 | * `--port`
54 | * Specify Serving PORT for LL-HLS
55 | * DEFAULT: 8080
56 |
57 | ### Example (Generate Test Stream H.265(libx265)/AAC With Timestamp)
58 |
59 | ```bash
60 | ffmpeg -re \
61 | -f lavfi -i testsrc=700x180:r=30000/1001 \
62 | -f lavfi -i sine=frequency=1000 \
63 | -vf "settb=AVTB,setpts='trunc(PTS/1K)*1K+st(1,trunc(RTCTIME/1K))-1K*trunc(ld(1)/1K)',drawtext=fontsize=60:fontcolor=black:text='%{localtime}.%{eif\:1M*t-1K*trunc(t*1K)\:d\:3}'" \
64 | -c:v libx265 -tune zerolatency -preset ultrafast -r 30 -g 15 -pix_fmt yuv420p \
65 | -c:a aac -ac 1 -ar 48000 \
66 | -f mpegts - | ./fmp4.py -t 1 -p 0.15 -w 10 --port 8080
67 | # watch http://localhost:8080/playlist.m3u8
68 | ```
69 |
70 | ## Special Thanks
71 |
72 | * [xtne6f](https://github.com/xtne6f): C++ ReImplementation ([tsmemseg](https://github.com/xtne6f/tsmemseg)) and Report ISOBMFF related misc bugs.
73 | * [tsukumi](https://github.com/tsukumijima): Very helpful advice and implementation to biim's Type Definition.
--------------------------------------------------------------------------------
/biim/mpeg2ts/pes.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from typing import Any
4 |
5 | class PES:
6 | HEADER_SIZE = 6
7 |
8 | def __init__(self, payload: bytes | bytearray | memoryview = b''):
9 | self.payload = memoryview(payload)
10 |
11 | def __getitem__(self, item: Any) -> Any:
12 | return self.payload[item]
13 |
14 | def __len__(self) -> int:
15 | return len(self.payload)
16 |
17 | def packet_start_code_prefix(self) -> int:
18 | return (self.payload[0] << 16) | (self.payload[1] << 8) | self.payload[2]
19 |
20 | def stream_id(self) -> int:
21 | return self.payload[3]
22 |
23 | def PES_packet_length(self) -> int:
24 | return (self.payload[4] << 8) | self.payload[5]
25 |
26 | def has_optional_pes_header(self) -> bool:
27 | if self.stream_id() in [0b10111100, 0b10111111, 0b11110000, 0b11110001, 0b11110010, 0b11111000, 0b11111111]:
28 | return False
29 | elif self.stream_id() in [0b10111110]:
30 | return False
31 | else:
32 | return True
33 |
34 | def has_pts(self) -> bool:
35 | if self.has_optional_pes_header():
36 | return (self.payload[PES.HEADER_SIZE + 1] & 0x80) != 0
37 | else:
38 | return False
39 |
40 | def has_dts(self) -> bool:
41 | if self.has_optional_pes_header():
42 | return (self.payload[PES.HEADER_SIZE + 1] & 0x40) != 0
43 | else:
44 | return False
45 |
46 | def pes_header_length(self) -> int | None:
47 | if self.has_optional_pes_header():
48 | return (self.payload[PES.HEADER_SIZE + 2])
49 | else:
50 | return None
51 |
52 | def pts(self) -> int | None:
53 | if not self.has_pts(): return None
54 |
55 | pts = 0
56 | pts <<= 3; pts |= ((self.payload[PES.HEADER_SIZE + 3 + 0] & 0x0E) >> 1)
57 | pts <<= 8; pts |= ((self.payload[PES.HEADER_SIZE + 3 + 1] & 0xFF) >> 0)
58 | pts <<= 7; pts |= ((self.payload[PES.HEADER_SIZE + 3 + 2] & 0xFE) >> 1)
59 | pts <<= 8; pts |= ((self.payload[PES.HEADER_SIZE + 3 + 3] & 0xFF) >> 0)
60 | pts <<= 7; pts |= ((self.payload[PES.HEADER_SIZE + 3 + 4] & 0xFE) >> 1)
61 | return pts
62 |
63 | def dts(self) -> int | None:
64 | if not self.has_dts(): return None
65 |
66 | dts = 0
67 | if self.has_pts():
68 | dts <<= 3; dts |= ((self.payload[PES.HEADER_SIZE + 8 + 0] & 0x0E) >> 1)
69 | dts <<= 8; dts |= ((self.payload[PES.HEADER_SIZE + 8 + 1] & 0xFF) >> 0)
70 | dts <<= 7; dts |= ((self.payload[PES.HEADER_SIZE + 8 + 2] & 0xFE) >> 1)
71 | dts <<= 8; dts |= ((self.payload[PES.HEADER_SIZE + 8 + 3] & 0xFF) >> 0)
72 | dts <<= 7; dts |= ((self.payload[PES.HEADER_SIZE + 8 + 4] & 0xFE) >> 1)
73 | else:
74 | dts <<= 3; dts |= ((self.payload[PES.HEADER_SIZE + 3 + 0] & 0x0E) >> 1)
75 | dts <<= 8; dts |= ((self.payload[PES.HEADER_SIZE + 3 + 1] & 0xFF) >> 0)
76 | dts <<= 7; dts |= ((self.payload[PES.HEADER_SIZE + 3 + 2] & 0xFE) >> 1)
77 | dts <<= 8; dts |= ((self.payload[PES.HEADER_SIZE + 3 + 3] & 0xFF) >> 0)
78 | dts <<= 7; dts |= ((self.payload[PES.HEADER_SIZE + 3 + 4] & 0xFE) >> 1)
79 |
80 | return dts
81 |
82 | def PES_packet_data(self) -> memoryview:
83 | if self.has_optional_pes_header():
84 | return self.payload[PES.HEADER_SIZE + 3 + self.payload[PES.HEADER_SIZE + 2]:]
85 | else:
86 | return self.payload[PES.HEADER_SIZE:]
87 |
--------------------------------------------------------------------------------
/biim/util/bytestream.py:
--------------------------------------------------------------------------------
1 | class ByteStream:
2 | def __init__(self, data: bytes | bytearray | memoryview):
3 | self.data = memoryview(data)
4 | self.current = 0
5 |
6 | def __len__(self):
7 | return max(0, len(self.data) - self.current)
8 |
9 | def __bool__(self):
10 | return len(self) > 0
11 |
12 | def read(self, size: int):
13 | if self.current + size > len(self.data):
14 | raise EOFError
15 | view = self.data[self.current: self.current + size]
16 | self.current = min(len(self.data), self.current + size)
17 | return view
18 |
19 | def readU8(self):
20 | if self.current + 1 > len(self.data):
21 | raise EOFError
22 | view = int.from_bytes(self.data[self.current: self.current + 1], byteorder='big')
23 | self.current = min(len(self.data), self.current + 1)
24 | return view
25 |
26 | def readU16(self):
27 | if self.current + 2 > len(self.data):
28 | raise EOFError
29 | view = int.from_bytes(self.data[self.current: self.current + 2], byteorder='big')
30 | self.current = min(len(self.data), self.current + 2)
31 | return view
32 |
33 | def readU24(self):
34 | if self.current + 3 > len(self.data):
35 | raise EOFError
36 | view = int.from_bytes(self.data[self.current: self.current + 3], byteorder='big')
37 | self.current = min(len(self.data), self.current + 3)
38 | return view
39 |
40 | def readU32(self):
41 | if self.current + 4 > len(self.data):
42 | raise EOFError
43 | view = int.from_bytes(self.data[self.current: self.current + 4], byteorder='big')
44 | self.current = min(len(self.data), self.current + 4)
45 | return view
46 |
47 | def readU64(self):
48 | if self.current + 8 > len(self.data):
49 | raise EOFError
50 | view = int.from_bytes(self.data[self.current: self.current + 8], byteorder='big')
51 | self.current = min(len(self.data), self.current + 8)
52 | return view
53 |
54 | def readS8(self):
55 | if self.current + 1 > len(self.data):
56 | raise EOFError
57 | view = int.from_bytes(self.data[self.current: self.current + 1], byteorder='big', signed=True)
58 | self.current = min(len(self.data), self.current + 1)
59 | return view
60 |
61 | def readS16(self):
62 | if self.current + 2 > len(self.data):
63 | raise EOFError
64 | view = int.from_bytes(self.data[self.current: self.current + 2], byteorder='big', signed=True)
65 | self.current = min(len(self.data), self.current + 2)
66 | return view
67 |
68 | def readS24(self):
69 | if self.current + 3 > len(self.data):
70 | raise EOFError
71 | view = int.from_bytes(self.data[self.current: self.current + 3], byteorder='big', signed=True)
72 | self.current = min(len(self.data), self.current + 3)
73 | return view
74 |
75 | def readS32(self):
76 | if self.current + 4 > len(self.data):
77 | raise EOFError
78 | view = int.from_bytes(self.data[self.current: self.current + 4], byteorder='big', signed=True)
79 | self.current = min(len(self.data), self.current + 4)
80 | return view
81 |
82 | def readS64(self):
83 | if self.current + 8 > len(self.data):
84 | raise EOFError
85 | view = int.from_bytes(self.data[self.current: self.current + 8], byteorder='big', signed=True)
86 | self.current = min(len(self.data), self.current + 8)
87 | return view
88 |
89 | def readAll(self):
90 | view = self.data[self.current: len(self.data)]
91 | self.current = len(self.data)
92 | return view
93 |
--------------------------------------------------------------------------------
/biim/rtmp/remuxer.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 | from biim.rtmp.demuxer import FLVDemuxer
4 |
5 | class FLVRemuxer(FLVDemuxer):
6 | def __init__(self, initial_track: int):
7 | self.next_track = initial_track
8 | self.video_tracks = dict[int, tuple[int, bytes]]() # FLV TrackId -> Track, Configuration
9 | self.audio_tracks = dict[int, tuple[int, bytes]]() # FLV TrackID -> Track, Configuration
10 |
11 | def onAVCDecoderConfigurationRecord(self, timestamp: int, track_id: int | None, data: memoryview):
12 | track = track_id + 1 if track_id is not None else 0
13 | if track in self.video_tracks:
14 | id, avcC = self.video_tracks[track]
15 | if avcC == data: return
16 |
17 | self.video_tracks[track] = (id, data)
18 | self.onTrackConfigurationChanged(timestamp, track, 'avc1', self.remuxAVCDecoderConfigurationRecord(track, data))
19 | return
20 |
21 | self.video_tracks[track] = (self.next_track, data)
22 | id = self.next_track
23 | self.next_track += 1
24 | self.onTrackAdded(timestamp, id, 'avc1', self.remuxAVCDecoderConfigurationRecord(track, data))
25 |
26 | def onAVCVideoData(self, timestamp: int, track_id: int | None, frame_type: int, cto: int, data: memoryview):
27 | track = track_id + 1 if track_id is not None else 0
28 | if track not in self.video_tracks: return
29 | id, _ = self.video_tracks[track]
30 | self.onMediaData(timestamp, id, 'avc1', self.remuxAVCVideoData(track, frame_type, cto, data))
31 |
32 | def onAVCEndOfSequence(self, timestamp: int, track_id: int | None):
33 | track = track_id + 1 if track_id is not None else 0
34 | if track not in self.video_tracks: return
35 | id, _ = self.video_tracks[track]
36 | self.onTrackRemoved(timestamp, id, 'avc1')
37 |
38 | @abstractmethod
39 | def remuxAVCDecoderConfigurationRecord(self, track: int, avcC: bytes | bytearray | memoryview) -> bytes:
40 | pass
41 |
42 | @abstractmethod
43 | def remuxAVCVideoData(self, track: int, frame_type: int, cto: int, data: bytes | bytearray | memoryview) -> bytes:
44 | pass
45 |
46 | @abstractmethod
47 | def onTrackAdded(self, timestamp: int, track: int, codec: str, remuxed: bytes):
48 | pass
49 |
50 | @abstractmethod
51 | def onTrackConfigurationChanged(self, timestamp: int, track: int, codec: str, remuxed: bytes):
52 | pass
53 |
54 | @abstractmethod
55 | def onTrackRemoved(self, timestamp: int, track: int, codec: str):
56 | pass
57 |
58 | @abstractmethod
59 | def onMediaData(self, timestamp: int, track: int, codec: str, remuxed: bytes):
60 | pass
61 |
62 | # TODO!!
63 | class FLVfMP4Remuxer(FLVRemuxer):
64 | def __init__(self):
65 | super().__init__(initial_track=1) # Track is track_id (fMP4)
66 |
67 | def remuxAVCDecoderConfigurationRecord(self, track: int, avcC: bytes | bytearray | memoryview) -> bytes:
68 | return b''
69 |
70 | def remuxAVCVideoData(self, track: int, frame_type: int, cto: int, data: bytes | bytearray | memoryview) -> bytes:
71 | return b''
72 |
73 | def onTrackAdded(self, timestamp: int, track: int, codec: str, remuxed: bytes):
74 | print('added', timestamp, track, codec, remuxed)
75 | pass
76 | def onTrackConfigurationChanged(self, timestamp: int, track: int, codec: str, remuxed: bytes):
77 | print('changed', timestamp, track, codec, remuxed)
78 | pass
79 | def onTrackRemoved(self, timestamp: int, codec: str, track: int):
80 | pass
81 | def onMediaData(self, timestamp: int, track: int, codec: str, remuxed: bytes):
82 | print('media', timestamp, track, codec, remuxed)
83 | pass
84 |
--------------------------------------------------------------------------------
/biim/hls/segment.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from typing import Iterator
4 | import asyncio
5 | from datetime import datetime, timedelta, timezone
6 |
7 | from biim.mpeg2ts import ts
8 |
9 | class PartialSegment:
10 | def __init__(self, beginPTS: int, isIFrame: bool = False):
11 | self.beginPTS: int = beginPTS
12 | self.endPTS: int | None = None
13 | self.hasIFrame: bool = isIFrame
14 | self.buffer: bytearray = bytearray()
15 | self.queues: list[asyncio.Queue[bytes | bytearray | memoryview | None]] = []
16 | self.m3u8s_with_skip: list[asyncio.Future[str]]= []
17 | self.m3u8s_without_skip: list[asyncio.Future[str]] = []
18 |
19 | def push(self, packet: bytes | bytearray | memoryview):
20 | self.buffer += packet
21 | for q in self.queues: q.put_nowait(packet)
22 |
23 | async def response(self) -> asyncio.Queue[bytes | bytearray | memoryview | None]:
24 | queue: asyncio.Queue[bytes | bytearray | memoryview | None] = asyncio.Queue()
25 |
26 | queue.put_nowait(self.buffer)
27 | if (self.isCompleted()):
28 | queue.put_nowait(None)
29 | else:
30 | self.queues.append(queue)
31 | return queue
32 |
33 | def m3u8(self, skip: bool = False) -> asyncio.Future[str]:
34 | f: asyncio.Future[str] = asyncio.Future()
35 | if not self.isCompleted():
36 | if skip: self.m3u8s_with_skip.append(f)
37 | else: self.m3u8s_without_skip.append(f)
38 | return f
39 |
40 | def complete(self, endPTS: int) -> None:
41 | self.endPTS = endPTS
42 | for q in self.queues: q.put_nowait(None)
43 | self.queues = []
44 |
45 | def notify(self, skipped_manifest: str, all_manifest: str) -> None:
46 | for f in self.m3u8s_with_skip:
47 | if not f.done(): f.set_result(skipped_manifest)
48 | self.m3u8s_with_skip = []
49 | for f in self.m3u8s_without_skip:
50 | if not f.done(): f.set_result(all_manifest)
51 | self.m3u8s_without_skip = []
52 |
53 | def isCompleted(self) -> bool:
54 | return self.endPTS is not None
55 |
56 | def extinf(self) -> timedelta | None:
57 | if self.endPTS is None:
58 | return None
59 | else:
60 | return timedelta(seconds = (((self.endPTS - self.beginPTS + ts.PCR_CYCLE) % ts.PCR_CYCLE) / ts.HZ))
61 |
62 | def estimate(self, endPTS: int) -> timedelta:
63 | return timedelta(seconds = (((endPTS - self.beginPTS + ts.PCR_CYCLE) % ts.PCR_CYCLE) / ts.HZ))
64 |
65 | class Segment(PartialSegment):
66 | def __init__(self, beginPTS, isIFrame = False, programDateTime = None):
67 | super().__init__(beginPTS, isIFrame = False)
68 | self.partials: list[PartialSegment] = [PartialSegment(beginPTS, isIFrame)]
69 | self.program_date_time: datetime = programDateTime or datetime.now(timezone.utc)
70 |
71 | def __iter__(self) -> Iterator[PartialSegment]:
72 | return iter(self.partials)
73 |
74 | def __len__(self) -> int:
75 | return len(self.partials)
76 |
77 | def push(self, packet: bytes | bytearray | memoryview) -> None:
78 | super().push(packet)
79 | if not self.partials: return
80 | self.partials[-1].push(packet)
81 |
82 | def completePartial(self, endPTS: int) -> None:
83 | if not self.partials: return
84 | self.partials[-1].complete(endPTS)
85 |
86 | def notifyPartial(self, skipped_manifest: str, all_manifest: str) -> None:
87 | if not self.partials: return
88 | self.partials[-1].notify(skipped_manifest, all_manifest)
89 |
90 | def newPartial(self, beginPTS: int, isIFrame: bool = False) -> None:
91 | self.partials.append(PartialSegment(beginPTS, isIFrame))
92 |
93 | def complete(self, endPTS: int) -> None:
94 | super().complete(endPTS)
95 | self.completePartial(endPTS)
96 |
97 | def notify(self, skipped_manifest: str, all_manifest: str) -> None:
98 | super().notify(skipped_manifest, all_manifest)
99 | self.notifyPartial(skipped_manifest, all_manifest)
100 |
101 |
--------------------------------------------------------------------------------
/biim/mpeg2ts/parser.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from collections import deque
4 | from typing import cast, Generic, Type, TypeVar, Iterator
5 |
6 | from biim.mpeg2ts import ts
7 | from biim.mpeg2ts.section import Section
8 | from biim.mpeg2ts.pes import PES
9 |
10 | SectionType = TypeVar('SectionType', bound=Section)
11 | PESType = TypeVar('PESType', bound=PES)
12 |
13 | class SectionParser(Generic[SectionType]):
14 | def __init__(self, _class: Type[Section] = Section):
15 | self.section: bytearray | None = None
16 | self.queue: deque[Section] = deque()
17 | self._class: Type[Section] = _class
18 |
19 | def __iter__(self) -> Iterator[SectionType]:
20 | return self
21 |
22 | def __next__(self) -> SectionType:
23 | if not self.queue:
24 | raise StopIteration()
25 | return cast(SectionType, self.queue.popleft())
26 |
27 | def push(self, packet: bytes | bytearray | memoryview) -> None:
28 | begin = ts.HEADER_SIZE + (1 + ts.adaptation_field_length(packet) if ts.has_adaptation_field(packet) else 0)
29 | if ts.payload_unit_start_indicator(packet): begin += 1
30 |
31 | if not self.section:
32 | if ts.payload_unit_start_indicator(packet):
33 | begin += ts.pointer_field(packet)
34 | else:
35 | return
36 |
37 | if ts.payload_unit_start_indicator(packet):
38 | while begin < ts.PACKET_SIZE:
39 | if packet[begin] == ts.STUFFING_BYTE[0]: break
40 | if self.section:
41 | next = min(begin + (((self.section[1] & 0x0F) << 8) | self.section[2]), ts.PACKET_SIZE)
42 | else:
43 | section_length = ((packet[begin + 1] & 0x0F) << 8) | packet[begin + 2]
44 | next = min(begin + (3 + section_length), ts.PACKET_SIZE)
45 | self.section = bytearray()
46 | self.section += (packet[begin:next])
47 |
48 | section_length = ((self.section[1] & 0x0F) << 8) | self.section[2]
49 | if len(self.section) == section_length + 3:
50 | self.queue.append(self._class(self.section))
51 | self.section = None
52 | elif len(self.section) > section_length + 3:
53 | self.section = None
54 |
55 | begin = next
56 | elif self.section is not None:
57 | section_length = ((self.section[1] & 0x0F) << 8) | self.section[2]
58 | remains = max(0, section_length + 3 - len(self.section))
59 |
60 | next = min(begin + remains, ts.PACKET_SIZE)
61 | self.section += packet[begin:next]
62 |
63 | if len(self.section) == section_length + 3:
64 | self.queue.append(self._class(self.section))
65 | self.section = None
66 | elif len(self.section) > section_length + 3:
67 | self.section = None
68 |
69 | class PESParser(Generic[PESType]):
70 | def __init__(self, _class: Type[PES] = PES):
71 | self.pes = None
72 | self.queue: deque[PES] = deque()
73 | self._class: Type[PES] = _class
74 |
75 | def __iter__(self) -> Iterator[PESType]:
76 | return self
77 |
78 | def __next__(self) -> PESType:
79 | if not self.queue:
80 | raise StopIteration()
81 | return cast(PESType, self.queue.popleft())
82 |
83 | def push(self, packet: bytes | bytearray | memoryview) -> None:
84 | begin = ts.HEADER_SIZE + (1 + ts.adaptation_field_length(packet) if ts.has_adaptation_field(packet) else 0)
85 | if not ts.payload_unit_start_indicator(packet) and not self.pes: return
86 |
87 | if ts.payload_unit_start_indicator(packet):
88 | if self.pes and ((self.pes[4] << 8) | self.pes[5]) == 0:
89 | self.queue.append(self._class(self.pes))
90 |
91 | pes_length = (packet[begin + 4] << 8) | packet[begin + 5]
92 | if pes_length == 0:
93 | next = ts.PACKET_SIZE
94 | else:
95 | next = min(begin + (PES.HEADER_SIZE + pes_length), ts.PACKET_SIZE)
96 | self.pes = bytearray(packet[begin:next])
97 | elif self.pes:
98 | pes_length = (self.pes[4] << 8) | self.pes[5]
99 | if pes_length == 0:
100 | next = ts.PACKET_SIZE
101 | else:
102 | next = min(begin + (PES.HEADER_SIZE + pes_length) - len(self.pes), ts.PACKET_SIZE)
103 | self.pes += packet[begin:next]
104 | else:
105 | return
106 |
107 | if ((self.pes[4] << 8) | self.pes[5]) > 0:
108 | if len(self.pes) == PES.HEADER_SIZE + (self.pes[4] << 8 | self.pes[5]):
109 | self.queue.append(self._class(self.pes))
110 | self.pes = None
111 | elif len(self.pes) > PES.HEADER_SIZE + (self.pes[4] << 8 | self.pes[5]):
112 | self.pes = None
113 |
--------------------------------------------------------------------------------
/biim/rtmp/amf0.py:
--------------------------------------------------------------------------------
1 | import io
2 | import struct
3 | from typing import cast, Any
4 |
5 | objectEnd = object()
6 |
7 | def parse(reader: io.BytesIO):
8 | while reader:
9 | tag_byte = reader.read(1)
10 | if tag_byte == b'':
11 | reader.close()
12 | return
13 | match int.from_bytes(tag_byte, byteorder='big'):
14 | case 0: # Number (8 bytes)
15 | return cast(float, struct.unpack('>d', reader.read(8))[0])
16 | case 1: # Boolean (1 bytes)
17 | return reader.read(1) != b'\x00'
18 | case 2: # String
19 | length = int.from_bytes(reader.read(2), byteorder='big')
20 | return reader.read(length).decode('utf-8')
21 | case 3: # Object
22 | result = dict()
23 | while True:
24 | length = int.from_bytes(reader.read(2), byteorder='big')
25 | name = reader.read(length).decode('utf-8')
26 | value = parse(reader)
27 | if value is objectEnd:
28 | return result
29 | result[name] = value
30 | case 4: # movie clip (reserved, not supported)
31 | reader.close()
32 | return None
33 | case 5: # null
34 | return None
35 | case 6: # undefined
36 | return None # for python convenience, undefined to None (same as null) conversion
37 | case 7: # reference
38 | #FIXME: I didn't see this tag
39 | reader.close()
40 | return None
41 | case 8: # ECMA Array
42 | result = dict()
43 | for _ in range(int.from_bytes(reader.read(4), byteorder='big') + 1): # +1: ObjectEnd used in librtmp for terminate ECMA Array
44 | length = int.from_bytes(reader.read(2), byteorder='big')
45 | name = reader.read(length).decode('utf-8')
46 | value = parse(reader)
47 | if value is objectEnd:
48 | return result
49 | result[name] = value
50 | return result
51 | case 9: # Object End
52 | return objectEnd
53 | case 10: # Strict Array
54 | length = int.from_bytes(reader.read(4), byteorder='big')
55 | return [parse(reader) for _ in range(length)]
56 | case 11: # Date
57 | timestamp = cast(float, struct.unpack('>d', reader.read(8)))
58 | timezone = int.from_bytes(reader.read(2), byteorder='big', signed=True) # should be set zero
59 | return timestamp + timezone
60 | case 12: # Long String
61 | length = int.from_bytes(reader.read(4), byteorder='big')
62 | return reader.read(length).decode('utf-8')
63 | case 13: # Unsupported
64 | #FIXME: I didn't see this tag
65 | reader.close()
66 | return None
67 | case 14: # Recordset (reserved, not supported)
68 | reader.close()
69 | return None
70 | case 15: # Xml Document
71 | #FIXME: I didn't see this tag
72 | reader.close()
73 | return None
74 | case 16: # Typed Object
75 | #FIXME: I didn't see this tag
76 | reader.close()
77 | return None
78 |
79 | reader.close()
80 | return None
81 |
82 | def deserialize(value: bytes | bytearray | memoryview):
83 | with io.BytesIO(value) as reader:
84 | result = []
85 | while reader:
86 | parsed = parse(reader)
87 | if reader.closed:
88 | return result
89 | result.append(parsed)
90 | return []
91 |
92 | def serialize(values: list[Any] | Any):
93 | amf = bytearray()
94 | for value in values if type(values) is list else [values]:
95 | if value is None:
96 | amf += b'\x05' # null used, undefined are not serialized
97 | elif type(value) == bytes or type(value) == bytearray or type(value) == memoryview:
98 | amf += value # already byteslike, so insert
99 | elif type(value) == int or type(value) == float:
100 | amf += b'\x00'
101 | amf += struct.pack('>d', float(value))
102 | elif type(value) == bool:
103 | amf += b'\x01'
104 | amf += b'\x01' if value else b'\x00'
105 | elif type(value) == str: # LongString or String
106 | if len(value) >= 0xFFFF: # Long String
107 | amf += b'\x0C'
108 | amf += len(value).to_bytes(4, byteorder='big')
109 | amf += value.encode('utf-8')
110 | else:
111 | amf += b'\x02' # String
112 | amf += len(value).to_bytes(2, byteorder='big')
113 | amf += value.encode('utf-8')
114 | elif type(value) == list: # Strict Array used
115 | amf += b'\x0A'
116 | amf += int.to_bytes(len(value), 4, byteorder='big')
117 | for _ in range(len(value)): amf += serialize([value])
118 | elif type(value) == dict: # Object used
119 | amf += b'\x03'
120 | for k, v in value.items():
121 | amf += len(k).to_bytes(2, byteorder='big')
122 | amf += k.encode('utf-8')
123 | amf += serialize([v])
124 | amf += b'\x00\x00\x09' # length = 0, name='', value=ObjectEnd
125 |
126 | return amf
127 |
128 |
--------------------------------------------------------------------------------
/pseudo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | biim MPEG-TS Streaming Demo
7 |
8 |
9 |
10 |
11 |
12 |
138 |
139 |
--------------------------------------------------------------------------------
/biim/variant/mpegts.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 |
3 | from biim.variant.handler import VariantHandler
4 | from biim.variant.codec import aac_codec_parameter_string
5 | from biim.variant.codec import avc_codec_parameter_string
6 | from biim.variant.codec import hevc_codec_parameter_string
7 |
8 | from biim.mpeg2ts import ts
9 | from biim.mpeg2ts.packetize import packetize_section, packetize_pes
10 | from biim.mpeg2ts.section import Section
11 | from biim.mpeg2ts.pes import PES
12 | from biim.mpeg2ts.h264 import H264PES
13 | from biim.mpeg2ts.h265 import H265PES
14 |
15 | AAC_SAMPLING_FREQUENCY = {
16 | 0x00: 96000,
17 | 0x01: 88200,
18 | 0x02: 64000,
19 | 0x03: 48000,
20 | 0x04: 44100,
21 | 0x05: 32000,
22 | 0x06: 24000,
23 | 0x07: 22050,
24 | 0x08: 16000,
25 | 0x09: 12000,
26 | 0x0a: 11025,
27 | 0x0b: 8000,
28 | 0x0c: 7350,
29 | }
30 |
31 | class MpegtsVariantHandler(VariantHandler):
32 |
33 | def __init__(self, target_duration: int, part_target: float, window_size: int | None = None, has_video: bool = True, has_audio: bool = True):
34 | super().__init__(target_duration, part_target, 'video/mp2t', window_size, False, has_video, has_audio)
35 | # PAT/PMT
36 | self.last_pat: Section | None = None
37 | self.last_pmt: Section | None = None
38 | self.pmt_pid: int | None = None
39 | self.pat_cc = 0
40 | self.pmt_cc = 0
41 | # Video Codec Specific
42 | self.h264_idr_detected = False
43 | self.h265_idr_detected = False
44 | self.h264_cc = 0
45 | self.h265_cc = 0
46 | # Audio Codec Specific
47 | self.aac_cc = 0
48 |
49 | def PAT(self, PAT: Section):
50 | if PAT.CRC32() != 0: return
51 | self.last_pat = PAT
52 |
53 | def PMT(self, pid: int, PMT: Section):
54 | if PMT.CRC32() != 0: return
55 | self.last_pmt = PMT
56 | self.pmt_pid = pid
57 |
58 | def update(self, new_segment: bool | None, timestamp: int, program_date_time: datetime) -> bool:
59 | if self.last_pat is None or self.last_pmt is None or self.pmt_pid is None: return False
60 | if not super().update(new_segment, timestamp, program_date_time): return False
61 |
62 | packets = packetize_section(self.last_pat, False, False, 0x00, 0, self.pat_cc)
63 | self.pat_cc = (self.pat_cc + len(packets)) & 0x0F
64 | for p in packets: self.m3u8.push(p)
65 | packets = packetize_section(self.last_pmt, False, False, self.pmt_pid, 0, self.pmt_cc)
66 | self.pmt_cc = (self.pmt_cc + len(packets)) & 0x0F
67 | for p in packets: self.m3u8.push(p)
68 | return True
69 |
70 | def h265(self, pid: int, h265: H265PES):
71 | if (timestamp := self.timestamp(h265.dts() or h265.pts())) is None: return
72 | if (program_date_time := self.program_date_time(h265.dts() or h265.pts())) is None: return
73 |
74 | hasIDR = False
75 | sps = None
76 | for ebsp in h265:
77 | nal_unit_type = (ebsp[0] >> 1) & 0x3f
78 |
79 | if nal_unit_type == 0x21: # SPS
80 | sps = ebsp
81 | elif nal_unit_type == 19 or nal_unit_type == 20 or nal_unit_type == 21: # IDR_W_RADL, IDR_W_LP, CRA_NUT
82 | hasIDR = True
83 |
84 | if sps and not self.video_codec.done():
85 | self.video_codec.set_result(hevc_codec_parameter_string(sps))
86 |
87 | self.h265_idr_detected |= hasIDR
88 | if not self.h265_idr_detected: return
89 |
90 | self.update(hasIDR, timestamp, program_date_time)
91 |
92 | packets = packetize_pes(h265, False, False, pid, 0, self.h265_cc)
93 | self.h265_cc = (self.h265_cc + len(packets)) & 0x0F
94 | for p in packets: self.m3u8.push(p)
95 |
96 | def h264(self, pid: int, h264: H264PES):
97 | if (timestamp := self.timestamp(h264.dts() or h264.pts())) is None: return
98 | if (program_date_time := self.program_date_time(h264.dts() or h264.pts())) is None: return
99 |
100 | hasIDR = False
101 | sps = None
102 | for ebsp in h264:
103 | nal_unit_type = ebsp[0] & 0x1f
104 |
105 | if nal_unit_type == 0x07: # SPS
106 | sps = ebsp
107 | elif nal_unit_type == 0x05:
108 | hasIDR = True
109 |
110 | if sps and not self.video_codec.done():
111 | self.video_codec.set_result(avc_codec_parameter_string(sps))
112 | self.h264_idr_detected |= hasIDR
113 | if not self.h264_idr_detected: return
114 |
115 | self.update(hasIDR, timestamp, program_date_time)
116 |
117 | packets = packetize_pes(h264, False, False, pid, 0, self.h264_cc)
118 | self.h264_cc = (self.h264_cc + len(packets)) & 0x0F
119 | for p in packets: self.m3u8.push(p)
120 |
121 | def aac(self, pid: int, aac: PES):
122 | if (timestamp := self.timestamp(aac.pts())) is None: return
123 | if (program_date_time := self.program_date_time(aac.pts())) is None: return
124 |
125 | if not self.has_video:
126 | self.update(None, timestamp, program_date_time)
127 |
128 | packets = packetize_pes(aac, False, False, pid, 0, self.aac_cc)
129 | self.aac_cc = (self.aac_cc + len(packets)) & 0x0F
130 | for p in packets: self.m3u8.push(p)
131 |
132 | begin, ADTS_AAC = 0, aac.PES_packet_data()
133 | length = len(ADTS_AAC)
134 | while begin < length:
135 | protection = (ADTS_AAC[begin + 1] & 0b00000001) == 0
136 | profile = ((ADTS_AAC[begin + 2] & 0b11000000) >> 6)
137 | samplingFrequencyIndex = ((ADTS_AAC[begin + 2] & 0b00111100) >> 2)
138 | channelConfiguration = ((ADTS_AAC[begin + 2] & 0b00000001) << 2) | ((ADTS_AAC[begin + 3] & 0b11000000) >> 6)
139 | frameLength = ((ADTS_AAC[begin + 3] & 0x03) << 11) | (ADTS_AAC[begin + 4] << 3) | ((ADTS_AAC[begin + 5] & 0xE0) >> 5)
140 | duration = 1024 * ts.HZ // AAC_SAMPLING_FREQUENCY[samplingFrequencyIndex]
141 |
142 | if not self.audio_codec.done():
143 | self.audio_codec.set_result(aac_codec_parameter_string(profile + 1))
144 |
145 | timestamp += duration
146 | program_date_time += timedelta(seconds=duration/ts.HZ)
147 | begin += frameLength
148 |
149 | def packet(self, packet: bytes | bytearray | memoryview):
150 | self.m3u8.push(packet)
151 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from typing import cast
4 |
5 | import asyncio
6 | from aiohttp import web
7 |
8 | import argparse
9 | import sys
10 | import os
11 | import time
12 |
13 | from biim.mpeg2ts import ts
14 | from biim.mpeg2ts.pat import PATSection
15 | from biim.mpeg2ts.pmt import PMTSection
16 | from biim.mpeg2ts.scte import SpliceInfoSection
17 | from biim.mpeg2ts.pes import PES
18 | from biim.mpeg2ts.h264 import H264PES
19 | from biim.mpeg2ts.h265 import H265PES
20 | from biim.mpeg2ts.parser import SectionParser, PESParser
21 |
22 | from biim.variant.mpegts import MpegtsVariantHandler
23 |
24 | from biim.util.reader import BufferingAsyncReader
25 |
26 | async def main():
27 | loop = asyncio.get_running_loop()
28 | parser = argparse.ArgumentParser(description=('biim: LL-HLS origin'))
29 |
30 | parser.add_argument('-i', '--input', type=argparse.FileType('rb'), nargs='?', default=sys.stdin.buffer)
31 | parser.add_argument('-s', '--SID', type=int, nargs='?')
32 | parser.add_argument('-w', '--window_size', type=int, nargs='?')
33 | parser.add_argument('-t', '--target_duration', type=int, nargs='?', default=1)
34 | parser.add_argument('-p', '--part_duration', type=float, nargs='?', default=0.1)
35 | parser.add_argument('--port', type=int, nargs='?', default=8080)
36 |
37 | args = parser.parse_args()
38 |
39 | handler = MpegtsVariantHandler(
40 | target_duration=args.target_duration,
41 | part_target=args.part_duration,
42 | window_size=args.window_size,
43 | has_video=True,
44 | has_audio=True,
45 | )
46 |
47 | # setup aiohttp
48 | app = web.Application()
49 | app.add_routes([
50 | web.get('/playlist.m3u8', handler.playlist),
51 | web.get('/segment', handler.segment),
52 | web.get('/part', handler.partial),
53 | ])
54 | runner = web.AppRunner(app)
55 | await runner.setup()
56 | await loop.create_server(cast(web.Server, runner.server), '0.0.0.0', args.port)
57 |
58 | # setup reader
59 | PAT_Parser: SectionParser[PATSection] = SectionParser(PATSection)
60 | PMT_Parser: SectionParser[PMTSection] = SectionParser(PMTSection)
61 | SCTE35_Parser: SectionParser[SpliceInfoSection] = SectionParser(SpliceInfoSection)
62 | H264_PES_Parser: PESParser[H264PES] = PESParser(H264PES)
63 | H265_PES_parser: PESParser[H265PES] = PESParser(H265PES)
64 | AAC_PES_Parser: PESParser[PES] = PESParser(PES)
65 |
66 | LATEST_VIDEO_TIMESTAMP: int | None = None
67 | LATEST_VIDEO_MONOTONIC_TIME: float | None = None
68 | LATEST_VIDEO_SLEEP_DIFFERENCE: float = 0
69 |
70 | PMT_PID: int | None = None
71 | H264_PID: int | None = None
72 | H265_PID: int | None = None
73 | AAC_PID: int | None = None
74 | SCTE35_PID: int | None = None
75 | PCR_PID: int | None = None
76 |
77 | if args.input is not sys.stdin.buffer or os.name == 'nt':
78 | reader = BufferingAsyncReader(args.input, ts.PACKET_SIZE * 16)
79 | else:
80 | reader = asyncio.StreamReader()
81 | protocol = asyncio.StreamReaderProtocol(reader)
82 | await loop.connect_read_pipe(lambda: protocol, args.input)
83 |
84 | while True:
85 | isEOF = False
86 | while True:
87 | sync_byte = await reader.read(1)
88 | if sync_byte == ts.SYNC_BYTE:
89 | break
90 | elif sync_byte == b'':
91 | isEOF = True
92 | break
93 | if isEOF:
94 | break
95 |
96 | packet = None
97 | try:
98 | packet = ts.SYNC_BYTE + await reader.readexactly(ts.PACKET_SIZE - 1)
99 | except asyncio.IncompleteReadError:
100 | break
101 |
102 | PID = ts.pid(packet)
103 | if PID == 0x00:
104 | PAT_Parser.push(packet)
105 | for PAT in PAT_Parser:
106 | if PAT.CRC32() != 0: continue
107 | handler.PAT(PAT)
108 |
109 | for program_number, program_map_PID in PAT:
110 | if program_number == 0: continue
111 |
112 | if program_number == args.SID:
113 | PMT_PID = program_map_PID
114 | elif not PMT_PID and not args.SID:
115 | PMT_PID = program_map_PID
116 |
117 | elif PID == PMT_PID:
118 | PMT_Parser.push(packet)
119 | for PMT in PMT_Parser:
120 | if PMT.CRC32() != 0: continue
121 | handler.PMT(PID, PMT)
122 |
123 | PCR_PID = PMT.PCR_PID
124 | for stream_type, elementary_PID, _ in PMT:
125 | if stream_type == 0x1b:
126 | H264_PID = elementary_PID
127 | elif stream_type == 0x24:
128 | H265_PID = elementary_PID
129 | elif stream_type == 0x86:
130 | SCTE35_PID = elementary_PID
131 |
132 | elif PID == H264_PID:
133 | H264_PES_Parser.push(packet)
134 | for H264 in H264_PES_Parser:
135 | handler.h264(PID, H264)
136 |
137 | if (timestamp := H264.dts() or H264.pts()) is None: continue
138 | if LATEST_VIDEO_TIMESTAMP is not None and LATEST_VIDEO_MONOTONIC_TIME is not None:
139 | TIMESTAMP_DIFF = ((timestamp - LATEST_VIDEO_TIMESTAMP + ts.PCR_CYCLE) % ts.PCR_CYCLE) / ts.HZ
140 | TIME_DIFF = time.monotonic() - LATEST_VIDEO_MONOTONIC_TIME
141 | if args.input is not sys.stdin.buffer:
142 | SLEEP_BEGIN = time.monotonic()
143 | await asyncio.sleep(max(0, TIMESTAMP_DIFF - (TIME_DIFF + LATEST_VIDEO_SLEEP_DIFFERENCE)))
144 | SLEEP_END = time.monotonic()
145 | LATEST_VIDEO_SLEEP_DIFFERENCE = (SLEEP_END - SLEEP_BEGIN) - max(0, TIMESTAMP_DIFF - (TIME_DIFF + LATEST_VIDEO_SLEEP_DIFFERENCE))
146 | LATEST_VIDEO_TIMESTAMP = timestamp
147 | LATEST_VIDEO_MONOTONIC_TIME = time.monotonic()
148 |
149 | elif PID == H265_PID:
150 | H265_PES_parser.push(packet)
151 | for H265 in H265_PES_parser:
152 | handler.h265(PID, H265)
153 |
154 | if (timestamp := H265.dts() or H265.pts()) is None: continue
155 | if LATEST_VIDEO_TIMESTAMP is not None and LATEST_VIDEO_MONOTONIC_TIME is not None:
156 | TIMESTAMP_DIFF = ((timestamp - LATEST_VIDEO_TIMESTAMP + ts.PCR_CYCLE) % ts.PCR_CYCLE) / ts.HZ
157 | TIME_DIFF = time.monotonic() - LATEST_VIDEO_MONOTONIC_TIME
158 | if args.input is not sys.stdin.buffer:
159 | SLEEP_BEGIN = time.monotonic()
160 | await asyncio.sleep(max(0, TIMESTAMP_DIFF - (TIME_DIFF + LATEST_VIDEO_SLEEP_DIFFERENCE)))
161 | SLEEP_END = time.monotonic()
162 | LATEST_VIDEO_SLEEP_DIFFERENCE = (SLEEP_END - SLEEP_BEGIN) - max(0, TIMESTAMP_DIFF - (TIME_DIFF + LATEST_VIDEO_SLEEP_DIFFERENCE))
163 | LATEST_VIDEO_TIMESTAMP = timestamp
164 | LATEST_VIDEO_MONOTONIC_TIME = time.monotonic()
165 |
166 | elif PID == AAC_PID:
167 | AAC_PES_Parser.push(packet)
168 | for AAC in AAC_PES_Parser:
169 | handler.aac(PID, AAC)
170 |
171 | elif PID == SCTE35_PID:
172 | handler.packet(packet)
173 | SCTE35_Parser.push(packet)
174 | for SCTE35 in SCTE35_Parser:
175 | if SCTE35.CRC32() != 0: continue
176 | handler.scte35(SCTE35)
177 |
178 | else:
179 | handler.packet(packet)
180 |
181 | if PID == PCR_PID and ts.has_pcr(packet):
182 | handler.pcr(cast(int, ts.pcr(packet)))
183 |
184 | if __name__ == '__main__':
185 | asyncio.run(main())
186 |
--------------------------------------------------------------------------------
/biim/mp4/avc.py:
--------------------------------------------------------------------------------
1 | from typing import cast
2 |
3 | from biim.mp4.box import trak, tkhd, mdia, mdhd, hdlr, minf, vmhd, dinf, stbl, stsd, avc1
4 | from biim.util.bitstream import BitStream
5 |
6 | escapes = set([0x00, 0x01, 0x02, 0x03])
7 |
8 | def ebsp2rbsp(data: bytes | bytearray | memoryview) -> bytes:
9 | rbsp = bytearray(data[:2])
10 | length = len(data)
11 | for index in range(2, length):
12 | if index < length - 1 and data[index - 2] == 0x00 and data[index - 1] == 0x00 and data[index + 0] == 0x03 and data[index + 1] in escapes:
13 | continue
14 | rbsp.append(data[index])
15 | return bytes(rbsp)
16 |
17 | def avcTrack(trackId: int, timescale: int, sps: bytes | bytearray | memoryview, pps: bytes | bytearray | memoryview) -> bytes:
18 | need_extra_fields = sps[3] not in [66, 77, 88]
19 | chroma_format_idc: int | None = None
20 | bit_depth_luma_minus8: int | None = None
21 | bit_depth_chroma_minus8: int | None = None
22 |
23 | codec_width: int | None = None
24 | codec_height: int | None = None
25 | presentation_width: int | None = None
26 | presentation_height: int | None = None
27 |
28 | def parseSPS():
29 | nonlocal chroma_format_idc
30 | nonlocal bit_depth_luma_minus8
31 | nonlocal bit_depth_chroma_minus8
32 |
33 | nonlocal codec_width
34 | nonlocal codec_height
35 | nonlocal presentation_width
36 | nonlocal presentation_height
37 |
38 | stream = BitStream(ebsp2rbsp(sps))
39 | stream.readByte() # remove header
40 |
41 | profile_idc = stream.readByte()
42 | stream.readByte()
43 | level_idc = stream.readByte()
44 | stream.readUEG()
45 |
46 | chroma_format_idc = 1
47 | chroma_format = 420
48 | chroma_format_table = [0, 420, 422, 444]
49 | bit_depth_luma_minus8 = 0
50 | bit_depth_chroma_minus8 = 0
51 | if profile_idc in [100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 144]:
52 | chroma_format_idc = stream.readUEG()
53 | if chroma_format_idc == 3:
54 | stream.readBits(1)
55 |
56 | bit_depth_luma_minus8 = stream.readUEG()
57 | bit_depth_chroma_minus8 = stream.readUEG()
58 | stream.readBits(1)
59 | if stream.readBool():
60 | scaling_list_count = 8 if chroma_format_idc != 3 else 12
61 | for i in range(scaling_list_count):
62 | if stream.readBool():
63 | count = 16 if i < 6 else 64
64 | last_scale, next_scale = 8, 8
65 | delta_scale = 0
66 | for _ in range(count):
67 | if next_scale != 0:
68 | delta_scale = stream.readSEG()
69 | next_scale = (last_scale + delta_scale + 256) % 256
70 | last_scale = last_scale if next_scale == 0 else next_scale
71 |
72 | stream.readUEG()
73 | pic_order_cnt_type = stream.readUEG()
74 | if pic_order_cnt_type == 0:
75 | stream.readUEG()
76 | elif pic_order_cnt_type == 1:
77 | stream.readBits(1)
78 | stream.readSEG()
79 | stream.readSEG()
80 | num_ref_frames_in_pic_order_cnt_cycle = stream.readUEG()
81 | for _ in range(num_ref_frames_in_pic_order_cnt_cycle): stream.readSEG()
82 | ref_frames = stream.readUEG()
83 | stream.readBits(1)
84 |
85 | pic_width_in_mbs_minus1 = stream.readUEG()
86 | pic_height_in_map_units_minus1 = stream.readUEG()
87 |
88 | frame_mbs_only_flag = stream.readBool()
89 | if frame_mbs_only_flag == 0: stream.readBits(1)
90 | stream.readBool()
91 |
92 | frame_crop_left_offset = 0
93 | frame_crop_right_offset = 0
94 | frame_crop_top_offset = 0
95 | frame_crop_bottom_offset = 0
96 | frame_cropping_flag = stream.readBool()
97 | if frame_cropping_flag:
98 | frame_crop_left_offset = stream.readUEG()
99 | frame_crop_right_offset = stream.readUEG()
100 | frame_crop_top_offset = stream.readUEG()
101 | frame_crop_bottom_offset = stream.readUEG()
102 |
103 | sar_width, sar_height = 1, 1
104 | fps, fps_fixed, fps_num, fps_den = 0, True, 0, 0
105 |
106 | vui_parameters_present_flag = stream.readBool()
107 | if vui_parameters_present_flag:
108 | if stream.readBool():
109 | aspect_ratio_idc = stream.readByte()
110 | sar_w_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2]
111 | sar_h_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1]
112 |
113 | if 0 < aspect_ratio_idc and aspect_ratio_idc <= 16:
114 | sar_width = sar_w_table[aspect_ratio_idc - 1]
115 | sar_height = sar_h_table[aspect_ratio_idc - 1]
116 | elif aspect_ratio_idc == 255:
117 | sar_width = stream.readByte() << 8 | stream.readByte()
118 | sar_height = stream.readByte() << 8 | stream.readByte()
119 |
120 | if stream.readBool():
121 | stream.readBool()
122 |
123 | if stream.readBool():
124 | stream.readBits(4)
125 | if stream.readBool():
126 | stream.readBits(24)
127 |
128 | if stream.readBool():
129 | stream.readUEG()
130 | stream.readUEG()
131 |
132 | if stream.readBool():
133 | num_units_in_tick = stream.readBits(32)
134 | time_scale = stream.readBits(32)
135 | fps_fixed = stream.readBool()
136 |
137 | fps_num = time_scale
138 | fps_den = num_units_in_tick * 2
139 | fps = fps_num / fps_den
140 |
141 | crop_unit_x, crop_unit_y = 0, 0
142 | if chroma_format_idc == 0:
143 | crop_unit_x = 1
144 | crop_unit_y = 2 - frame_mbs_only_flag
145 | else:
146 | sub_wc = 1 if chroma_format_idc == 3 else 2
147 | sub_hc = 2 if chroma_format_idc == 1 else 1
148 | crop_unit_x = sub_wc
149 | crop_unit_y = sub_hc * (2 - frame_mbs_only_flag)
150 |
151 | codec_width = (pic_width_in_mbs_minus1 + 1) * 16
152 | codec_height = (2 - frame_mbs_only_flag) * ((pic_height_in_map_units_minus1 + 1) * 16)
153 | codec_width -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x
154 | codec_height -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y
155 |
156 | presentation_width = (codec_width * sar_width + (sar_height - 1)) // sar_height
157 | presentation_height = codec_height
158 | parseSPS()
159 |
160 | avcC = b''.join([
161 | bytes([
162 | 0x01, # configurationVersion
163 | sps[1], # AVCProfileIndication
164 | sps[2], # profile_compatibility
165 | sps[3], # AVCLevelIndication
166 | 0xFF, # 111111 + lengthSizeMinusOne(3)
167 | ]),
168 | bytes([
169 | 0xE0 | 0x01, # 111 + numOfSequenceParameterSets
170 | ]),
171 | (len(sps)).to_bytes(2, byteorder='big'),
172 | sps,
173 | bytes([
174 | 0x01, # numOfPictureParameterSets
175 | ]),
176 | (len(pps)).to_bytes(2, byteorder='big'),
177 | pps,
178 | bytes([]) if not need_extra_fields else bytes([
179 | 0xFC | cast(int, chroma_format_idc),
180 | 0xF8 | cast(int, bit_depth_luma_minus8),
181 | 0xF8 | cast(int, bit_depth_chroma_minus8),
182 | 0x00
183 | ])
184 | ])
185 |
186 | return trak(
187 | tkhd(trackId, cast(int, presentation_width), cast(int, presentation_height)),
188 | mdia(
189 | mdhd(timescale),
190 | hdlr('vide', 'videoHandler'),
191 | minf(
192 | vmhd(),
193 | dinf(),
194 | stbl(
195 | stsd(
196 | avc1(avcC, cast(int, codec_width), cast(int, codec_height))
197 | )
198 | )
199 | )
200 | )
201 | )
202 |
--------------------------------------------------------------------------------
/fmp4.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from typing import cast
4 |
5 | import asyncio
6 | from aiohttp import web
7 |
8 | import argparse
9 | import sys
10 | import os
11 | import time
12 |
13 | from biim.mpeg2ts import ts
14 | from biim.mpeg2ts.pat import PATSection
15 | from biim.mpeg2ts.pmt import PMTSection
16 | from biim.mpeg2ts.scte import SpliceInfoSection
17 | from biim.mpeg2ts.pes import PES
18 | from biim.mpeg2ts.h264 import H264PES
19 | from biim.mpeg2ts.h265 import H265PES
20 | from biim.mpeg2ts.parser import SectionParser, PESParser
21 |
22 | from biim.variant.fmp4 import Fmp4VariantHandler
23 |
24 | from biim.util.reader import BufferingAsyncReader
25 |
26 | async def main():
27 | loop = asyncio.get_running_loop()
28 | parser = argparse.ArgumentParser(description=('biim: LL-HLS origin'))
29 |
30 | parser.add_argument('-i', '--input', type=argparse.FileType('rb'), nargs='?', default=sys.stdin.buffer)
31 | parser.add_argument('-s', '--SID', type=int, nargs='?')
32 | parser.add_argument('-w', '--window_size', type=int, nargs='?')
33 | parser.add_argument('-t', '--target_duration', type=int, nargs='?', default=1)
34 | parser.add_argument('-p', '--part_duration', type=float, nargs='?', default=0.1)
35 | parser.add_argument('--port', type=int, nargs='?', default=8080)
36 |
37 | args = parser.parse_args()
38 |
39 | handler = Fmp4VariantHandler(
40 | target_duration=args.target_duration,
41 | part_target=args.part_duration,
42 | window_size=args.window_size,
43 | has_video=True,
44 | has_audio=True,
45 | )
46 |
47 | # setup aiohttp
48 | app = web.Application()
49 | app.add_routes([
50 | web.get('/playlist.m3u8', handler.playlist),
51 | web.get('/segment', handler.segment),
52 | web.get('/part', handler.partial),
53 | web.get('/init', handler.initialization),
54 | ])
55 | runner = web.AppRunner(app)
56 | await runner.setup()
57 | await loop.create_server(cast(web.Server, runner.server), '0.0.0.0', args.port)
58 |
59 | # setup reader
60 | PAT_Parser: SectionParser[PATSection] = SectionParser(PATSection)
61 | PMT_Parser: SectionParser[PMTSection] = SectionParser(PMTSection)
62 | AAC_PES_Parser: PESParser[PES] = PESParser(PES)
63 | H264_PES_Parser: PESParser[H264PES] = PESParser(H264PES)
64 | H265_PES_Parser: PESParser[H265PES] = PESParser(H265PES)
65 | ID3_PES_Parser: PESParser[PES] = PESParser(PES)
66 | SCTE35_Parser: SectionParser[SpliceInfoSection] = SectionParser(SpliceInfoSection)
67 |
68 | LATEST_VIDEO_TIMESTAMP_90KHZ: int | None = None
69 | LATEST_VIDEO_MONOTONIC_TIME: float | None = None
70 | LATEST_VIDEO_SLEEP_DIFFERENCE: float = 0
71 |
72 | PMT_PID: int | None = None
73 | AAC_PID: int | None = None
74 | H264_PID: int | None = None
75 | H265_PID: int | None = None
76 | ID3_PID: int | None = None
77 | SCTE35_PID: int | None = None
78 | PCR_PID: int | None = None
79 |
80 | if args.input is not sys.stdin.buffer or os.name == 'nt':
81 | reader = BufferingAsyncReader(args.input, ts.PACKET_SIZE * 16)
82 | else:
83 | reader = asyncio.StreamReader()
84 | protocol = asyncio.StreamReaderProtocol(reader)
85 | await loop.connect_read_pipe(lambda: protocol, args.input)
86 |
87 | while True:
88 | isEOF = False
89 | while True:
90 | sync_byte = await reader.read(1)
91 | if sync_byte == ts.SYNC_BYTE:
92 | break
93 | elif sync_byte == b'':
94 | isEOF = True
95 | break
96 | if isEOF:
97 | break
98 |
99 | packet = None
100 | try:
101 | packet = ts.SYNC_BYTE + await reader.readexactly(ts.PACKET_SIZE - 1)
102 | except asyncio.IncompleteReadError:
103 | break
104 |
105 | PID = ts.pid(packet)
106 | if PID == H264_PID:
107 | H264_PES_Parser.push(packet)
108 | for H264 in H264_PES_Parser:
109 | handler.h264(H264)
110 |
111 | if (timestamp := H264.dts() or H264.pts()) is None: continue
112 | if LATEST_VIDEO_TIMESTAMP_90KHZ is not None and LATEST_VIDEO_MONOTONIC_TIME is not None:
113 | TIMESTAMP_DIFF = ((timestamp - LATEST_VIDEO_TIMESTAMP_90KHZ + ts.PCR_CYCLE) % ts.PCR_CYCLE) / ts.HZ
114 | TIME_DIFF = time.monotonic() - LATEST_VIDEO_MONOTONIC_TIME
115 | if args.input is not sys.stdin.buffer:
116 | SLEEP_BEGIN = time.monotonic()
117 | await asyncio.sleep(max(0, TIMESTAMP_DIFF - (TIME_DIFF + LATEST_VIDEO_SLEEP_DIFFERENCE)))
118 | SLEEP_END = time.monotonic()
119 | LATEST_VIDEO_SLEEP_DIFFERENCE = (SLEEP_END - SLEEP_BEGIN) - max(0, TIMESTAMP_DIFF - (TIME_DIFF + LATEST_VIDEO_SLEEP_DIFFERENCE))
120 | LATEST_VIDEO_TIMESTAMP_90KHZ = timestamp
121 | LATEST_VIDEO_MONOTONIC_TIME = time.monotonic()
122 |
123 | elif PID == H265_PID:
124 | H265_PES_Parser.push(packet)
125 | for H265 in H265_PES_Parser:
126 | handler.h265(H265)
127 |
128 | if (timestamp := H265.dts() or H265.pts()) is None: continue
129 | if LATEST_VIDEO_TIMESTAMP_90KHZ is not None and LATEST_VIDEO_MONOTONIC_TIME is not None:
130 | TIMESTAMP_DIFF = ((timestamp - LATEST_VIDEO_TIMESTAMP_90KHZ + ts.PCR_CYCLE) % ts.PCR_CYCLE) / ts.HZ
131 | TIME_DIFF = time.monotonic() - LATEST_VIDEO_MONOTONIC_TIME
132 | if args.input is not sys.stdin.buffer:
133 | SLEEP_BEGIN = time.monotonic()
134 | await asyncio.sleep(max(0, TIMESTAMP_DIFF - (TIME_DIFF + LATEST_VIDEO_SLEEP_DIFFERENCE)))
135 | SLEEP_END = time.monotonic()
136 | LATEST_VIDEO_SLEEP_DIFFERENCE = (SLEEP_END - SLEEP_BEGIN) - max(0, TIMESTAMP_DIFF - (TIME_DIFF + LATEST_VIDEO_SLEEP_DIFFERENCE))
137 | LATEST_VIDEO_TIMESTAMP_90KHZ = timestamp
138 | LATEST_VIDEO_MONOTONIC_TIME = time.monotonic()
139 |
140 | elif PID == AAC_PID:
141 | AAC_PES_Parser.push(packet)
142 | for AAC in AAC_PES_Parser:
143 | handler.aac(AAC)
144 |
145 | elif PID == 0x00:
146 | PAT_Parser.push(packet)
147 | for PAT in PAT_Parser:
148 | if PAT.CRC32() != 0: continue
149 |
150 | for program_number, program_map_PID in PAT:
151 | if program_number == 0: continue
152 |
153 | if program_number == args.SID:
154 | PMT_PID = program_map_PID
155 | elif not PMT_PID and not args.SID:
156 | PMT_PID = program_map_PID
157 |
158 | elif PID == PMT_PID:
159 | PMT_Parser.push(packet)
160 | for PMT in PMT_Parser:
161 | if PMT.CRC32() != 0: continue
162 |
163 | PCR_PID = PMT.PCR_PID
164 | for stream_type, elementary_PID, _ in PMT:
165 | if stream_type == 0x1b:
166 | H264_PID = elementary_PID
167 | elif stream_type == 0x24:
168 | H265_PID = elementary_PID
169 | elif stream_type == 0x0F:
170 | AAC_PID = elementary_PID
171 | elif stream_type == 0x15:
172 | ID3_PID = elementary_PID
173 | elif stream_type == 0x86:
174 | SCTE35_PID = elementary_PID
175 |
176 | elif PID == ID3_PID:
177 | ID3_PES_Parser.push(packet)
178 | for ID3 in ID3_PES_Parser:
179 | handler.id3(ID3)
180 |
181 | elif PID == SCTE35_PID:
182 | SCTE35_Parser.push(packet)
183 | for SCTE35 in SCTE35_Parser:
184 | if SCTE35.CRC32() != 0: continue
185 | handler.scte35(SCTE35)
186 |
187 | else:
188 | pass
189 |
190 | if PID == PCR_PID and ts.has_pcr(packet):
191 | handler.pcr(cast(int, ts.pcr(packet)))
192 |
193 | if __name__ == '__main__':
194 | asyncio.run(main())
195 |
--------------------------------------------------------------------------------
/multi.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from typing import cast, Callable, Any
4 |
5 | import asyncio
6 | from aiohttp import web
7 |
8 | import argparse
9 | import sys
10 | import os
11 |
12 | from biim.mpeg2ts import ts
13 | from biim.mpeg2ts.pat import PATSection
14 | from biim.mpeg2ts.pmt import PMTSection
15 | from biim.mpeg2ts.scte import SpliceInfoSection
16 | from biim.mpeg2ts.pes import PES
17 | from biim.mpeg2ts.h264 import H264PES
18 | from biim.mpeg2ts.h265 import H265PES
19 | from biim.mpeg2ts.parser import SectionParser, PESParser
20 |
21 | from biim.variant.fmp4 import Fmp4VariantHandler
22 |
23 | from biim.util.reader import BufferingAsyncReader
24 |
25 | async def setup(port: int, prefix: str = '', all_handlers: list[tuple[int, Fmp4VariantHandler]] = [], all_video_handlers: list[tuple[int, Fmp4VariantHandler]] = [], all_audio_handler: list[tuple[int, Fmp4VariantHandler]] = []):
26 | # setup aiohttp
27 | loop = asyncio.get_running_loop()
28 | app = web.Application()
29 |
30 | async def master(request: web.Request):
31 | m3u8 = '#EXTM3U\n#EXT-X-VERSION:3\n\n'
32 |
33 | has_audio = bool(all_audio_handler)
34 | for index, (pid, handler) in enumerate(all_audio_handler):
35 | name = ['Primary', 'Secondary'][index] if index < 2 else f'Other-{index}'
36 | m3u8 += f'#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="{name}",DEFAULT={"YES" if index == 0 else "NO"},AUTOSELECT=YES,LANGUAGE="JPN",URI="./{pid}/playlist.m3u8"\n'
37 | if has_audio: m3u8 += '\n'
38 |
39 | for pid, handler in all_video_handlers:
40 | m3u8 += f'#EXT-X-STREAM-INF:BANDWIDTH={await handler.bandwidth()},CODECS="{await handler.codec()}"' + (',AUDIO="audio"' if has_audio else '') +'\n'
41 | m3u8 += f'./{pid}/playlist.m3u8\n'
42 |
43 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=36000'}, text=m3u8, content_type="application/x-mpegURL")
44 |
45 | app.add_routes(
46 | sum(
47 | [
48 | [web.get(f'{prefix}/master.m3u8', master)]
49 | ] + [
50 | [
51 | web.get(f'{prefix}/{pid}/playlist.m3u8', handler.playlist),
52 | web.get(f'{prefix}/{pid}/segment', handler.segment),
53 | web.get(f'{prefix}/{pid}/part', handler.partial),
54 | web.get(f'{prefix}/{pid}/init', handler.initialization),
55 | ] for pid, handler in all_handlers
56 | ],
57 | []
58 | )
59 | )
60 | runner = web.AppRunner(app)
61 | await runner.setup()
62 | await loop.create_server(cast(web.Server, runner.server), '0.0.0.0', port)
63 |
64 | async def main():
65 | parser = argparse.ArgumentParser(description=('biim: LL-HLS origin'))
66 |
67 | parser.add_argument('-i', '--input', type=argparse.FileType('rb'), nargs='?', default=sys.stdin.buffer)
68 | parser.add_argument('-s', '--SID', type=int, nargs='?')
69 | parser.add_argument('-w', '--window_size', type=int, nargs='?')
70 | parser.add_argument('-t', '--target_duration', type=int, nargs='?', default=1)
71 | parser.add_argument('-p', '--part_duration', type=float, nargs='?', default=0.1)
72 | parser.add_argument('--port', type=int, nargs='?', default=8080)
73 |
74 | args = parser.parse_args()
75 | loop = asyncio.get_running_loop()
76 |
77 | PMT_VERSION = None
78 | PMT_PID: int | None = None
79 | PCR_PID: int | None = None
80 |
81 | PAT_Parser: SectionParser[PATSection] = SectionParser(PATSection)
82 | PMT_Parser: SectionParser[PMTSection] = SectionParser(PMTSection)
83 |
84 | cb: dict[int, tuple[PESParser | SectionParser, Callable[[Any], Any]]] = dict()
85 |
86 | ALL_HANDLER: list[tuple[int, Fmp4VariantHandler]] = []
87 | ALL_VIDEO_HANDLER: list[tuple[int, Fmp4VariantHandler]] = []
88 | ALL_AUDIO_HANDLER: list[tuple[int, Fmp4VariantHandler]] = []
89 | def ID3_CALLBACK(ID3: PES):
90 | for _, handler in ALL_VIDEO_HANDLER: handler.id3(ID3)
91 | def SCTE35_CALLBACK(SCTE35: SpliceInfoSection):
92 | if SCTE35.CRC32() != 0: return
93 | for _, handler in ALL_HANDLER: handler.scte35(SCTE35)
94 |
95 | # setup reader
96 | if args.input is not sys.stdin.buffer or os.name == 'nt':
97 | reader = BufferingAsyncReader(args.input, ts.PACKET_SIZE * 16)
98 | else:
99 | reader = asyncio.StreamReader()
100 | protocol = asyncio.StreamReaderProtocol(reader)
101 | await loop.connect_read_pipe(lambda: protocol, args.input)
102 |
103 | while True:
104 | isEOF = False
105 | while True:
106 | sync_byte = await reader.read(1)
107 | if sync_byte == ts.SYNC_BYTE:
108 | break
109 | elif sync_byte == b'':
110 | isEOF = True
111 | break
112 | if isEOF:
113 | break
114 |
115 | packet = None
116 | try:
117 | packet = ts.SYNC_BYTE + await reader.readexactly(ts.PACKET_SIZE - 1)
118 | except asyncio.IncompleteReadError:
119 | break
120 |
121 | PID = ts.pid(packet)
122 |
123 | if PID == 0x00:
124 | PAT_Parser.push(packet)
125 | for PAT in PAT_Parser:
126 | if PAT.CRC32() != 0: continue
127 |
128 | for program_number, program_map_PID in PAT:
129 | if program_number == 0: continue
130 |
131 | if program_number == args.SID:
132 | PMT_PID = program_map_PID
133 | elif not PMT_PID and not args.SID:
134 | PMT_PID = program_map_PID
135 |
136 | elif PID == PMT_PID:
137 | PMT_Parser.push(packet)
138 | for PMT in PMT_Parser:
139 | if PMT.CRC32() != 0: continue
140 | if PMT.version_number() == PMT_VERSION: continue
141 | PMT_VERSION = PMT.version_number()
142 |
143 | cb = dict()
144 | ALL_HANDLER = []
145 | ALL_VIDEO_HANDLER = []
146 | ALL_AUDIO_HANDLER = []
147 |
148 | PCR_PID = PMT.PCR_PID
149 | for stream_type, elementary_PID, _ in PMT:
150 | if stream_type == 0x1b:
151 | handler = Fmp4VariantHandler(target_duration=args.target_duration, part_target=args.part_duration, window_size=args.window_size, has_video=True, has_audio=False)
152 | ALL_HANDLER.append((elementary_PID, handler))
153 | ALL_VIDEO_HANDLER.append((elementary_PID, handler))
154 | cb[elementary_PID] = (PESParser[H264PES](H264PES), handler.h264)
155 | elif stream_type == 0x24:
156 | handler = Fmp4VariantHandler(target_duration=args.target_duration, part_target=args.part_duration, window_size=args.window_size, has_video=True, has_audio=False)
157 | ALL_HANDLER.append((elementary_PID, handler))
158 | ALL_VIDEO_HANDLER.append((elementary_PID, handler))
159 | cb[elementary_PID] = (PESParser[H265PES](H265PES), handler.h265)
160 | elif stream_type == 0x0F:
161 | handler = Fmp4VariantHandler(target_duration=args.target_duration, part_target=args.part_duration, window_size=args.window_size, has_video=False, has_audio=True)
162 | ALL_HANDLER.append((elementary_PID, handler))
163 | ALL_AUDIO_HANDLER.append((elementary_PID, handler))
164 | cb[elementary_PID] = (PESParser[PES](PES), handler.aac)
165 | elif stream_type == 0x15:
166 | cb[elementary_PID] = (PESParser[PES](PES), ID3_CALLBACK)
167 | elif stream_type == 0x86:
168 | cb[elementary_PID] = (SectionParser[SpliceInfoSection](SpliceInfoSection), SCTE35_CALLBACK)
169 |
170 | for _, handler in ALL_VIDEO_HANDLER:
171 | handler.set_renditions([f'../{pid}/playlist.m3u8' for pid, r in ALL_VIDEO_HANDLER if r != handler])
172 | for _, handler in ALL_AUDIO_HANDLER:
173 | handler.set_renditions([f'../{pid}/playlist.m3u8' for pid, r in ALL_AUDIO_HANDLER if r != handler])
174 | await setup(args.port, '', ALL_HANDLER, ALL_VIDEO_HANDLER, ALL_AUDIO_HANDLER)
175 |
176 | elif PID in cb:
177 | cb[PID][0].push(packet)
178 | for data in cb[PID][0]: cb[PID][1](data)
179 |
180 | else:
181 | pass
182 |
183 | if PID == PCR_PID and ts.has_pcr(packet):
184 | for _, handler in ALL_HANDLER: handler.pcr(cast(int, ts.pcr(packet)))
185 |
186 | if __name__ == '__main__':
187 | asyncio.run(main())
188 |
--------------------------------------------------------------------------------
/biim/rtmp/rtmp.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import random
3 | from dataclasses import dataclass
4 | from itertools import batched
5 | from enum import Enum, auto
6 |
7 | import biim.rtmp.amf0 as amf0
8 |
9 | STREAM_TYPE_ID_AUDIO = 0x08
10 | STREAM_TYPE_ID_VIDEO = 0x09
11 | STREAM_TYPE_ID_DATA = 0x12
12 | STREAM_TYPE_ID_FOR_MEDIA = set([STREAM_TYPE_ID_AUDIO, STREAM_TYPE_ID_VIDEO, STREAM_TYPE_ID_DATA])
13 |
14 | @dataclass
15 | class Message:
16 | message_type_id: int
17 | message_stream_id: int
18 | message_length: int
19 | timestamp: int
20 | chunk: bytes
21 |
22 | async def receive_message(reader: asyncio.StreamReader):
23 | chunk_length = 128 # Maximum Chunk length (initial value: 128)
24 | chunk_memory: dict[int, Message] = dict()
25 |
26 | try:
27 | while not reader.at_eof():
28 | first = int.from_bytes(await reader.readexactly(1), byteorder='big')
29 | fmt = (first & 0xC0) >> 6
30 | cs_id = first & 0x3F
31 | if cs_id == 0:
32 | cs_id = 64 + int.from_bytes(await reader.readexactly(1), byteorder='little')
33 | elif cs_id == 1:
34 | cs_id = 64 + int.from_bytes(await reader.readexactly(2), byteorder='little')
35 |
36 | # determine timestamp
37 | extended_timestamp = False
38 | timestamp = chunk_memory[cs_id].timestamp if cs_id in chunk_memory else None
39 | if fmt in [0, 1, 2]: # has timestamp
40 | timestamp = int.from_bytes(await reader.readexactly(3), byteorder='big')
41 | if timestamp >= 0xFFFFFF: extended_timestamp = True # has extended1 timestamp
42 | elif fmt in [1, 2]: # has delta timestampe
43 | timestamp += chunk_memory[cs_id].timestamp
44 | elif timestamp is None: # when reference previous timestamp is missing, ignore it
45 | continue
46 |
47 | # determine message_length and message_type_id
48 | if fmt in [0, 1]:
49 | message_length = int.from_bytes(await reader.readexactly(3), byteorder='big')
50 | message_type_id = int.from_bytes(await reader.readexactly(1), byteorder='big')
51 | else:
52 | message_length = chunk_memory[cs_id].message_length
53 | message_type_id = chunk_memory[cs_id].message_type_id
54 |
55 | # determine message_stream_id
56 | if fmt == 0:
57 | message_stream_id = int.from_bytes(await reader.readexactly(4), byteorder='little')
58 | else:
59 | message_stream_id = chunk_memory[cs_id].message_stream_id
60 |
61 | # build next chunk
62 | if fmt == 3:
63 | chunk = chunk_memory[cs_id].chunk if cs_id in chunk_memory else b''
64 | else:
65 | chunk = b''
66 |
67 | # determine extended timestamp
68 | if extended_timestamp:
69 | timestamp = int.from_bytes(await reader.readexactly(4), byteorder='big')
70 | if fmt in [1, 2]: # has delta timestampe
71 | timestamp += chunk_memory[cs_id].timestamp
72 |
73 | # chunk_lenght is maximum value, so terminate early can occured
74 | chunk_memory[cs_id] = Message(message_type_id, message_stream_id, message_length, timestamp, chunk + (await reader.readexactly(min(message_length - len(chunk), chunk_length))))
75 | if chunk_memory[cs_id].message_length <= len(chunk_memory[cs_id].chunk):
76 | if chunk_memory[cs_id].message_type_id == 1: # "Set Chunk Size" message recieved, slightly change chunk_length (librtmp and obs compatible)
77 | chunk_length = int.from_bytes(chunk_memory[cs_id].chunk, byteorder='big')
78 | else: # other message are propagate
79 | yield chunk_memory[cs_id]
80 | chunk_memory[cs_id].chunk = b'' # diffent message but same length, this case also fmt = 3 used, so clear flushing fmt = 3 for new message
81 |
82 | except asyncio.IncompleteReadError:
83 | return
84 |
85 | async def send_message(writer: asyncio.StreamWriter, message: Message):
86 | chunk_length = 128 # Maximum Chunk length (initial value: 128)
87 | for index, splitted in enumerate(batched(message.chunk, chunk_length)):
88 | splitted = bytes(splitted)
89 | chunk = bytearray()
90 | fmt = 0 if index == 0 else 3
91 | chunk += bytes([(fmt << 6) | 2]) # for convenience, cs_id send always 2
92 |
93 | extended_timestamp = False
94 | if fmt == 0:
95 | extended_timestamp = message.timestamp >= 0xFFFFFF
96 | chunk += int.to_bytes(min(message.timestamp, 0xFFFFFF), 3, byteorder='big') # timestamp
97 | chunk += int.to_bytes(message.message_length, 3, byteorder='big') # message_length
98 | chunk += int.to_bytes(message.message_type_id, 1, byteorder='big') # message_type_id
99 | chunk += int.to_bytes(message.message_stream_id, 4, byteorder='little')
100 | if extended_timestamp:
101 | chunk += int.to_bytes(message.timestamp, 4, byteorder='big') # extended timestamp
102 | # concat chunk content
103 | chunk += splitted
104 | # send!
105 | writer.write(chunk)
106 | await writer.drain()
107 |
108 | class RecieverState(Enum):
109 | WAITING_CONNECT = auto()
110 | WAITING_FCPUBLISH = auto()
111 | WAITING_CREATESTREAM = auto()
112 | WAITING_PUBLISH = auto()
113 | RECEIVING = auto()
114 |
115 | async def recieve(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, appName: str, streamKey: str):
116 | # Process Handshake
117 | try:
118 | while not reader.at_eof():
119 | # C0/S0
120 | await reader.readexactly(1) # C0
121 | writer.write(b'\x03') # S0
122 | await writer.drain()
123 | # C1/S1
124 | s1_random = random.randbytes(1536 - 4 * 2) # random(1528 bytes)
125 | writer.write(bytes(8) + s1_random) # S1 = time(4 bytes) + zero(4 bytes) + random(1528 bytes)
126 | await writer.drain()
127 | c1 = await reader.readexactly(1536) # C1 = time(4 bytes) + zero(4 bytes) + random(1528 bytes)
128 | c1_time = int.from_bytes(c1[0:4], byteorder='big')
129 | c1_random = c1[8:]
130 | # C2/S2
131 | writer.write(c1_time.to_bytes(4, byteorder='big') + bytes(4) + c1_random) # S2 = time(4 bytes) + time2(4 bytes) + random echo(1528 bytes)
132 | await writer.drain()
133 | c2_echo = (await reader.readexactly(1536))[8:] # C2 = time(4 bytes) + time2(4 bytes) + random echo(1528 bytes)
134 | if s1_random == c2_echo: break # Success Handshake
135 | # Failed Handshake, so continue
136 | except asyncio.IncompleteReadError:
137 | return
138 |
139 | state = RecieverState.WAITING_CONNECT
140 |
141 | async for recieved in receive_message(reader):
142 | match state:
143 | case RecieverState.WAITING_CONNECT:
144 | if recieved.message_type_id != 20: continue
145 | amf = amf0.deserialize(recieved.chunk)
146 | if amf[0] != 'connect': continue
147 | transaction_id = amf[1]
148 | if appName != amf[2]['app']: return # Close Connection
149 |
150 | # TODO: need sescription each parameter!
151 | connect_result = amf0.serialize([
152 | '_result',
153 | transaction_id, # The callee reference number
154 | {
155 | 'fmsVer': 'FMS/3,5,7,7009',
156 | 'capabilities': 31,
157 | 'mode': 1,
158 | }, {
159 | 'code': 'NetConnection.Connect.Success', # Important
160 | 'description': 'Connection succeeded.', # Any String
161 | 'data': {
162 | 'version': '3,5,7,7009',
163 | },
164 | 'objectEncoding': 0, # connection AMF Object Type, 0 => AMF0, 3 => AMF3
165 | 'level': 'status',
166 | }
167 | ])
168 | await send_message(writer, Message(20, 0, len(connect_result), 0, connect_result))
169 | state = RecieverState.WAITING_FCPUBLISH
170 |
171 | case RecieverState.WAITING_FCPUBLISH:
172 | if recieved.message_type_id != 20: continue
173 | amf = amf0.deserialize(recieved.chunk)
174 | if amf[0] != 'FCPublish': continue
175 | if streamKey != amf[3]: return # Close Connection
176 |
177 | state = RecieverState.WAITING_CREATESTREAM
178 |
179 | case RecieverState.WAITING_CREATESTREAM:
180 | if recieved.message_type_id != 20: continue
181 | amf = amf0.deserialize(recieved.chunk)
182 | if amf[0] != 'createStream': continue
183 | transaction_id = amf[1]
184 |
185 | create_stream_result = amf0.serialize([
186 | '_result',
187 | transaction_id, # The callee reference number
188 | None,
189 | 1 # stream_id (0 and 2 is reserved, so 1 used)
190 | ])
191 | await send_message(writer, Message(20, 0, len(create_stream_result), 0, create_stream_result))
192 | state = RecieverState.WAITING_PUBLISH
193 |
194 | case RecieverState.WAITING_PUBLISH:
195 | if recieved.message_type_id != 20: continue
196 | amf = amf0.deserialize(recieved.chunk)
197 | if amf[0] != 'publish': continue
198 | transaction_id = amf[1]
199 |
200 | publish_result = amf0.serialize([
201 | 'onStatus',
202 | transaction_id, # The callee reference number
203 | None,
204 | {
205 | 'code': 'NetStream.Publish.Start', # Important
206 | 'description': 'Publish Accepted', # Any String
207 | 'level': 'status'
208 | }
209 | ])
210 | await send_message(writer, Message(20, 0, len(publish_result), 0, publish_result))
211 | state = RecieverState.RECEIVING
212 |
213 | case RecieverState.RECEIVING:
214 | # Propagate Video/Audio/Metadata
215 | if recieved.message_type_id in STREAM_TYPE_ID_FOR_MEDIA:
216 | yield recieved
217 |
218 |
--------------------------------------------------------------------------------
/biim/variant/fmp4.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timedelta
2 |
3 | from biim.variant.handler import VariantHandler
4 | from biim.variant.codec import aac_codec_parameter_string
5 | from biim.variant.codec import avc_codec_parameter_string
6 | from biim.variant.codec import hevc_codec_parameter_string
7 |
8 | from biim.mpeg2ts import ts
9 | from biim.mpeg2ts.pes import PES
10 | from biim.mpeg2ts.h264 import H264PES
11 | from biim.mpeg2ts.h265 import H265PES
12 | from biim.mp4.box import ftyp, moov, mvhd, mvex, trex, moof, mdat, emsg
13 | from biim.mp4.avc import avcTrack
14 | from biim.mp4.hevc import hevcTrack
15 | from biim.mp4.mp4a import mp4aTrack
16 |
17 | AAC_SAMPLING_FREQUENCY = {
18 | 0x00: 96000,
19 | 0x01: 88200,
20 | 0x02: 64000,
21 | 0x03: 48000,
22 | 0x04: 44100,
23 | 0x05: 32000,
24 | 0x06: 24000,
25 | 0x07: 22050,
26 | 0x08: 16000,
27 | 0x09: 12000,
28 | 0x0a: 11025,
29 | 0x0b: 8000,
30 | 0x0c: 7350,
31 | }
32 |
33 | class Fmp4VariantHandler(VariantHandler):
34 |
35 | def __init__(self, target_duration: int, part_target: float, window_size: int | None = None, has_video: bool = True, has_audio: bool = True):
36 | super().__init__(target_duration, part_target, 'video/mp4', window_size, True, has_video, has_audio)
37 | # M3U8 Tracks
38 | self.audio_track: bytes | None = None
39 | self.video_track: bytes | None = None
40 | # Video Codec Specific
41 | self.h264_idr_detected = False
42 | self.h265_idr_detected = False
43 | self.curr_h264: tuple[bool, bytearray, int, int, datetime] | None = None # hasIDR, mdat, timestamp, cts, program_date_time
44 | self.curr_h265: tuple[bool, bytearray, int, int, datetime] | None = None # hasIDR, mdat, timestamp, cts, program_date_time
45 | # Audio Codec Specific
46 | self.last_aac_timestamp = None
47 |
48 | def h265(self, h265: H265PES):
49 | if (dts := h265.dts() or h265.pts()) is None: return
50 | if (pts := h265.pts()) is None: return
51 | cto = (pts - dts + ts.PCR_CYCLE) % ts.PCR_CYCLE
52 | if (timestamp := self.timestamp(dts)) is None: return
53 | if (program_date_time := self.program_date_time(dts)) is None: return
54 |
55 | hasIDR = False
56 | content = bytearray()
57 | vps, sps, pps = None, None, None
58 | for ebsp in h265:
59 | nal_unit_type = (ebsp[0] >> 1) & 0x3f
60 |
61 | if nal_unit_type == 0x20: # VPS
62 | vps = ebsp
63 | elif nal_unit_type == 0x21: # SPS
64 | sps = ebsp
65 | elif nal_unit_type == 0x22: # PPS
66 | pps = ebsp
67 | elif nal_unit_type == 0x23 or nal_unit_type == 0x27: # AUD or SEI
68 | pass
69 | elif nal_unit_type == 19 or nal_unit_type == 20 or nal_unit_type == 21: # IDR_W_RADL, IDR_W_LP, CRA_NUT
70 | hasIDR = True
71 | content += len(ebsp).to_bytes(4, byteorder='big') + ebsp
72 | else:
73 | content += len(ebsp).to_bytes(4, byteorder='big') + ebsp
74 | if sps and not self.video_codec.done():
75 | self.video_codec.set_result(hevc_codec_parameter_string(sps))
76 | if vps and sps and pps:
77 | self.video_track = hevcTrack(1, ts.HZ, vps, sps, pps)
78 |
79 | if self.init and not self.init.done() and self.video_track:
80 | if not self.has_audio:
81 | self.init.set_result(b''.join([
82 | ftyp(),
83 | moov(
84 | mvhd(ts.HZ),
85 | mvex([
86 | trex(1),
87 | ]),
88 | b''.join([
89 | self.video_track
90 | ])
91 | )
92 | ]))
93 | elif self.audio_track:
94 | self.init.set_result(b''.join([
95 | ftyp(),
96 | moov(
97 | mvhd(ts.HZ),
98 | mvex([
99 | trex(1),
100 | trex(2),
101 | ]),
102 | b''.join([
103 | self.video_track,
104 | self.audio_track
105 | ])
106 | )
107 | ]))
108 |
109 | next_h265 = (hasIDR, content, timestamp, cto, program_date_time)
110 |
111 | if not self.curr_h265:
112 | self.curr_h265 = next_h265
113 | return
114 |
115 | next_timestamp = timestamp
116 | hasIDR, content, timestamp, cto, program_date_time = self.curr_h265
117 | duration = next_timestamp - timestamp
118 | self.curr_h265 = next_h265
119 |
120 | self.h265_idr_detected|= hasIDR
121 | if not self.h265_idr_detected: return
122 |
123 | self.update(hasIDR, timestamp, program_date_time)
124 | self.m3u8.push(
125 | b''.join([
126 | moof(0,
127 | [
128 | (1, duration, timestamp, 0, [(len(content), duration, hasIDR, cto)])
129 | ]
130 | ),
131 | mdat(content)
132 | ])
133 | )
134 |
135 | def h264(self, h264: H264PES):
136 | if (dts := h264.dts() or h264.pts()) is None: return
137 | if (pts := h264.pts()) is None: return
138 | cto = (pts - dts + ts.PCR_CYCLE) % ts.PCR_CYCLE
139 | if (timestamp := self.timestamp(dts)) is None: return
140 | if (program_date_time := self.program_date_time(dts)) is None: return
141 |
142 | hasIDR = False
143 | content = bytearray()
144 | sps, pps = None, None
145 | for ebsp in h264:
146 | nal_unit_type = ebsp[0] & 0x1f
147 |
148 | if nal_unit_type == 0x07: # SPS
149 | sps = ebsp
150 | elif nal_unit_type == 0x08: # PPS
151 | pps = ebsp
152 | elif nal_unit_type == 0x09 or nal_unit_type == 0x06: # AUD or SEI
153 | pass
154 | elif nal_unit_type == 0x05:
155 | hasIDR = True
156 | content += len(ebsp).to_bytes(4, byteorder='big') + ebsp
157 | else:
158 | content += len(ebsp).to_bytes(4, byteorder='big') + ebsp
159 |
160 | if sps and not self.video_codec.done():
161 | self.video_codec.set_result(avc_codec_parameter_string(sps))
162 | if sps and pps:
163 | self.video_track = avcTrack(1, ts.HZ, sps, pps)
164 |
165 | if self.init and not self.init.done() and self.video_track:
166 | if not self.has_audio:
167 | self.init.set_result(b''.join([
168 | ftyp(),
169 | moov(
170 | mvhd(ts.HZ),
171 | mvex([
172 | trex(1),
173 | ]),
174 | b''.join([
175 | self.video_track
176 | ])
177 | )
178 | ]))
179 | elif self.audio_track:
180 | self.init.set_result(b''.join([
181 | ftyp(),
182 | moov(
183 | mvhd(ts.HZ),
184 | mvex([
185 | trex(1),
186 | trex(2),
187 | ]),
188 | b''.join([
189 | self.video_track,
190 | self.audio_track
191 | ])
192 | )
193 | ]))
194 |
195 | next_h264 = (hasIDR, content, timestamp, cto, program_date_time)
196 |
197 | if not self.curr_h264:
198 | self.curr_h264 = next_h264
199 | return
200 |
201 | next_timestamp = timestamp
202 | hasIDR, content, timestamp, cto, program_date_time = self.curr_h264
203 | duration = next_timestamp - timestamp
204 | self.curr_h264 = next_h264
205 |
206 | self.h264_idr_detected|= hasIDR
207 | if not self.h264_idr_detected: return
208 |
209 | self.update(hasIDR, timestamp, program_date_time)
210 | self.m3u8.push(
211 | b''.join([
212 | moof(0,
213 | [
214 | (1, duration, timestamp, 0, [(len(content), duration, hasIDR, cto)])
215 | ]
216 | ),
217 | mdat(content)
218 | ])
219 | )
220 |
221 | def aac(self, aac: PES):
222 | if (timestamp := self.timestamp(aac.pts())) is None: return
223 | if (program_date_time := self.program_date_time(aac.pts())) is None: return
224 |
225 | begin, ADTS_AAC = 0, aac.PES_packet_data()
226 | length = len(ADTS_AAC)
227 | while begin < length:
228 | protection = (ADTS_AAC[begin + 1] & 0b00000001) == 0
229 | profile = ((ADTS_AAC[begin + 2] & 0b11000000) >> 6)
230 | samplingFrequencyIndex = ((ADTS_AAC[begin + 2] & 0b00111100) >> 2)
231 | channelConfiguration = ((ADTS_AAC[begin + 2] & 0b00000001) << 2) | ((ADTS_AAC[begin + 3] & 0b11000000) >> 6)
232 | frameLength = ((ADTS_AAC[begin + 3] & 0x03) << 11) | (ADTS_AAC[begin + 4] << 3) | ((ADTS_AAC[begin + 5] & 0xE0) >> 5)
233 | duration = 1024 * ts.HZ // AAC_SAMPLING_FREQUENCY[samplingFrequencyIndex]
234 |
235 | if not self.audio_codec.done():
236 | self.audio_codec.set_result(aac_codec_parameter_string(profile + 1))
237 |
238 | if not self.audio_track:
239 | config = bytes([
240 | ((profile + 1) << 3) | ((samplingFrequencyIndex & 0x0E) >> 1),
241 | ((samplingFrequencyIndex & 0x01) << 7) | (channelConfiguration << 3)
242 | ])
243 | self.audio_track = mp4aTrack(2, ts.HZ, config, channelConfiguration, AAC_SAMPLING_FREQUENCY[samplingFrequencyIndex])
244 |
245 | if self.init and not self.init.done() and self.audio_track:
246 | if not self.has_video:
247 | self.init.set_result(b''.join([
248 | ftyp(),
249 | moov(
250 | mvhd(ts.HZ),
251 | mvex([
252 | trex(2)
253 | ]),
254 | b''.join([
255 | self.audio_track
256 | ])
257 | )
258 | ]))
259 |
260 | if not self.has_video:
261 | self.update(None, timestamp, program_date_time)
262 |
263 | self.m3u8.push(
264 | b''.join([
265 | moof(0,
266 | [
267 | (2, duration, timestamp, 0, [(frameLength - (9 if protection else 7), duration, False, 0)])
268 | ]
269 | ),
270 | mdat(bytes(ADTS_AAC[begin + (9 if protection else 7): begin + frameLength]))
271 | ])
272 | )
273 |
274 | timestamp += duration
275 | program_date_time += timedelta(seconds=duration/ts.HZ)
276 | begin += frameLength
277 |
278 | def id3(self, id3: PES):
279 | if (timestamp := self.timestamp(id3.pts())) is None: return
280 | self.m3u8.push(emsg(ts.HZ, timestamp, None, 'https://aomedia.org/emsg/ID3', id3.PES_packet_data()))
281 |
282 |
--------------------------------------------------------------------------------
/biim/hls/m3u8.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import asyncio
4 | import math
5 | from collections import deque
6 | from datetime import datetime, timedelta
7 |
8 | from typing import Any, cast
9 |
10 | from biim.hls.segment import Segment
11 |
12 | class Daterange:
13 | def __init__(self, id: str, start_date: datetime, end_date: datetime | None = None, **kwargs):
14 | self.id: str = id
15 | self.start_date: datetime = start_date
16 | self.end_date: datetime | None = end_date
17 | self.attributes: dict[str, Any] = kwargs
18 |
19 | def close(self, end_date: datetime) -> None:
20 | if self.end_date is not None: return
21 | self.end_date = end_date
22 |
23 | def __str__(self) -> str:
24 | duration = (self.end_date - self.start_date).total_seconds() if self.end_date else None
25 | attributes = ','.join([f'{attr}={value}' for attr, value in self.attributes.items()])
26 | attributes = ',' + attributes if attributes else ''
27 |
28 | return ''.join([
29 | f'#EXT-X-DATERANGE:ID="{self.id}",START-DATE="{self.start_date.isoformat()}"{attributes}\n',
30 | (f'#EXT-X-DATERANGE:ID="{self.id}",START-DATE="{self.start_date.isoformat()}",END-DATE="{self.end_date.isoformat()},DURATION={duration}"\n' if self.end_date is not None else '')
31 | ])
32 |
33 | class M3U8:
34 | def __init__(self, *, target_duration: int, part_target: float, window_size: int | None = None, has_init: bool = False):
35 | self.media_sequence: int = 0
36 | self.target_duration: int = target_duration
37 | self.part_target: float = part_target
38 | self.window_size: int | None = window_size
39 | self.has_init: bool = has_init
40 | self.renditions: list[str] = []
41 | self.dateranges: dict[str, Daterange] = dict()
42 | self.segments: deque[Segment] = deque()
43 | self.outdated: deque[Segment] = deque()
44 | self.published: bool = False
45 | self.futures: list[asyncio.Future[str]] = []
46 | self.bitrate = asyncio.Future[int]()
47 |
48 | def set_renditions(self, renditions: list[str]):
49 | self.renditions = renditions
50 |
51 | def report(self) -> str | None:
52 | segment_index = len(self.segments) - 1
53 | while segment_index >= 0:
54 | part_index = len(self.segments[segment_index].partials) - 1
55 | while part_index >= 0:
56 | if self.segments[segment_index].partials[part_index].isCompleted():
57 | return f'LAST-MSN={self.media_sequence + segment_index},LAST-PART={part_index}'
58 | part_index -= 1
59 | segment_index -= 1
60 | return None
61 |
62 | def in_range(self, msn: int) -> bool:
63 | return self.media_sequence <= msn and msn < self.media_sequence + len(self.segments)
64 |
65 | def in_outdated(self, msn: int) -> bool:
66 | return self.media_sequence > msn and msn >= self.media_sequence - len(self.outdated)
67 |
68 | def plain(self) -> asyncio.Future[str] | None:
69 | f: asyncio.Future[str] = asyncio.Future()
70 | if self.published:
71 | f.set_result(self.manifest())
72 | else:
73 | self.futures.append(f)
74 | return f
75 |
76 | def blocking(self, msn: int, part: int | None, skip: bool = False) -> asyncio.Future[str] | None:
77 | if not self.in_range(msn): return None
78 |
79 | index = msn - self.media_sequence
80 |
81 | if part is None:
82 | f = self.segments[index].m3u8(skip)
83 | if self.segments[index].isCompleted():
84 | f.set_result(self.manifest(skip))
85 | else:
86 | if part > len(self.segments[index].partials): return None
87 |
88 | f = self.segments[index].partials[part].m3u8(skip)
89 | if self.segments[index].partials[part].isCompleted():
90 | f.set_result(self.manifest(skip))
91 | return f
92 |
93 | def push(self, packet: bytes | bytearray | memoryview) -> None:
94 | if not self.segments: return
95 | self.segments[-1].push(packet)
96 |
97 | def newSegment(self, beginPTS: int, isIFrame: bool = False, programDateTime: datetime | None = None) -> None:
98 | self.segments.append(Segment(beginPTS, isIFrame, programDateTime))
99 | while self.window_size is not None and self.window_size < len(self.segments):
100 | self.outdated.appendleft(self.segments.popleft())
101 | self.media_sequence += 1
102 | while self.window_size is not None and self.window_size < len(self.outdated):
103 | self.outdated.pop()
104 |
105 | def newPartial(self, beginPTS: int, isIFrame: bool = False) -> None:
106 | if not self.segments: return
107 | self.segments[-1].newPartial(beginPTS, isIFrame)
108 |
109 | def completeSegment(self, endPTS: int) -> None:
110 | self.published = True
111 |
112 | if not self.segments: return
113 | self.segments[-1].complete(endPTS)
114 | self.segments[-1].notify(self.manifest(True), self.manifest(False))
115 | for f in self.futures:
116 | if not f.done(): f.set_result(self.manifest())
117 | self.futures = []
118 | if self.bitrate.done(): return
119 | self.bitrate.set_result(int(len(self.segments[-1].buffer) * 8 / cast(timedelta, self.segments[-1].extinf()).total_seconds()))
120 |
121 | def completePartial(self, endPTS: int) -> None:
122 | if not self.segments: return
123 | self.segments[-1].completePartial(endPTS)
124 | self.segments[-1].notify(self.manifest(True), self.manifest(False))
125 |
126 | def continuousSegment(self, endPTS: int, isIFrame: bool = False, programDateTime: datetime | None = None) -> None:
127 | lastSegment = self.segments[-1] if self.segments else None
128 | self.newSegment(endPTS, isIFrame, programDateTime)
129 |
130 | if not lastSegment: return
131 | self.published = True
132 | lastSegment.complete(endPTS)
133 | lastSegment.notify(self.manifest(True), self.manifest(False))
134 | for f in self.futures:
135 | if not f.done(): f.set_result(self.manifest())
136 | self.futures = []
137 | if self.bitrate.done(): return
138 | self.bitrate.set_result(int(len(lastSegment.buffer) * 8 / cast(timedelta, lastSegment.extinf()).total_seconds()))
139 |
140 | def continuousPartial(self, endPTS: int, isIFrame: bool = False) -> None:
141 | lastSegment = self.segments[-1] if self.segments else None
142 | lastPartial = lastSegment.partials[-1] if lastSegment else None
143 | self.newPartial(endPTS, isIFrame)
144 |
145 | if not lastPartial: return
146 | lastPartial.complete(endPTS)
147 | lastPartial.notify(self.manifest(True), self.manifest(False))
148 |
149 | async def segment(self, msn: int) -> asyncio.Queue[bytes | bytearray | memoryview | None] | None:
150 | if not self.in_range(msn):
151 | if not self.in_outdated(msn): return None
152 | index = (self.media_sequence - msn) - 1
153 | return await self.outdated[index].response()
154 | index = msn - self.media_sequence
155 | return await self.segments[index].response()
156 |
157 | async def partial(self, msn: int, part: int) -> asyncio.Queue[bytes | bytearray | memoryview | None] | None:
158 | if not self.in_range(msn):
159 | if not self.in_outdated(msn): return None
160 | index = (self.media_sequence - msn) - 1
161 | if part > len(self.outdated[index].partials): return None
162 | return await self.outdated[index].partials[part].response()
163 | index = msn - self.media_sequence
164 | if part > len(self.segments[index].partials): return None
165 | return await self.segments[index].partials[part].response()
166 |
167 | async def bandwidth(self) -> int:
168 | return await self.bitrate
169 |
170 | def open(self, id: str, start_date: datetime, end_date: datetime | None = None, **kwargs):
171 | if id in self.dateranges: return
172 | self.dateranges[id] = Daterange(id, start_date, end_date, **kwargs)
173 |
174 | def close(self, id: str, end_date: datetime):
175 | if id not in self.dateranges: return
176 | self.dateranges[id].close(end_date)
177 |
178 | def estimated_tartget_duration(self) -> int:
179 | target_duration = self.target_duration
180 | for segment in self.segments:
181 | if not segment.isCompleted(): continue
182 | target_duration = max(target_duration, math.ceil(cast(timedelta, segment.extinf()).total_seconds()))
183 | return target_duration
184 |
185 | def manifest(self, skip: bool = False) -> str:
186 | m3u8 = ''
187 | m3u8 += f'#EXTM3U\n'
188 | m3u8 += f'#EXT-X-VERSION:{9 if self.window_size is None else 6}\n'
189 | m3u8 += f'#EXT-X-TARGETDURATION:{self.estimated_tartget_duration()}\n'
190 | m3u8 += f'#EXT-X-PART-INF:PART-TARGET={self.part_target:.06f}\n'
191 | if self.window_size is None:
192 | m3u8 += f'#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK={(self.part_target * 3.001):.06f},CAN-SKIP-UNTIL={self.estimated_tartget_duration() * 6}\n'
193 | m3u8 += f'#EXT-X-PLAYLIST-TYPE:EVENT\n'
194 | else:
195 | m3u8 += f'#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK={(self.part_target * 3.001):.06f}\n'
196 | m3u8 += f'#EXT-X-MEDIA-SEQUENCE:{self.media_sequence}\n'
197 |
198 | if self.has_init:
199 | m3u8 += f'\n'
200 | m3u8 += f'#EXT-X-MAP:URI="init"\n'
201 |
202 | if len(self.segments) > 0:
203 | for id in list(self.dateranges.keys()):
204 | end_date = self.dateranges[id].end_date
205 | if end_date is None: continue
206 | if end_date >= self.segments[0].program_date_time: continue
207 | del self.dateranges[id]
208 |
209 | skip_end_index = 0
210 | if skip:
211 | elapsed = 0
212 | for seg_index, segment in enumerate(reversed(self.segments)):
213 | seg_index = (len(self.segments) - 1) - seg_index
214 | if not segment.isCompleted(): continue
215 | elapsed += cast(timedelta, segment.extinf()).total_seconds()
216 | if elapsed >= self.estimated_tartget_duration() * 6:
217 | skip_end_index = seg_index
218 | break
219 | if skip_end_index > 0:
220 | m3u8 += f'\n'
221 | m3u8 += f'#EXT-X-SKIP:SKIPPED-SEGMENTS={skip_end_index}\n'
222 |
223 | for daterange in self.dateranges.values():
224 | m3u8 += f'\n'
225 | m3u8 += f'{daterange}'
226 |
227 | for seg_index, segment in enumerate(self.segments):
228 | if seg_index < skip_end_index: continue # SKIP
229 | msn = self.media_sequence + seg_index
230 | m3u8 += f'\n'
231 | m3u8 += f'#EXT-X-PROGRAM-DATE-TIME:{segment.program_date_time.isoformat()}\n'
232 | if seg_index >= len(self.segments) - 4:
233 | for part_index, partial in enumerate(segment):
234 | hasIFrame = ',INDEPENDENT=YES' if partial.hasIFrame else ''
235 | if not partial.isCompleted():
236 | m3u8 += f'#EXT-X-PRELOAD-HINT:TYPE=PART,URI="part?msn={msn}&part={part_index}"{hasIFrame}\n'
237 | else:
238 | m3u8 += f'#EXT-X-PART:DURATION={cast(timedelta, partial.extinf()).total_seconds():.06f},URI="part?msn={msn}&part={part_index}"{hasIFrame}\n'
239 |
240 | if segment.isCompleted():
241 | m3u8 += f'#EXTINF:{cast(timedelta, segment.extinf()).total_seconds():.06f}\n'
242 | m3u8 += f'segment?msn={msn}\n'
243 |
244 | if not self.renditions: return m3u8
245 | if (redintion_report := self.report()) is None: return m3u8
246 | m3u8 += f'\n'
247 | for path in self.renditions:
248 | m3u8 += f'#EXT-X-RENDITION-REPORT:URI="{path}",{redintion_report}\n'
249 |
250 | return m3u8
251 |
--------------------------------------------------------------------------------
/biim/mpeg2ts/scte.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | from typing import Type
4 |
5 | from biim.util.bitstream import BitStream
6 | from biim.mpeg2ts.section import Section
7 |
8 | class SpliceInfoSection(Section):
9 | SPLICE_NULL = 0x00
10 | SPLICE_SCHEDULE = 0x04
11 | SPLICE_INSERT = 0x05
12 | TIME_SIGNAL = 0x06
13 | BANDWIDTH_RESERVATION = 0x07
14 | PRIVATE_COMMAND = 0xFF
15 |
16 | def __init__(self, payload=b''):
17 | super().__init__(payload)
18 | bitstream = BitStream(payload[Section.BASIC_HEADER_SIZE:])
19 | self.protocol_version = bitstream.readBits(8)
20 | self.encrypted_packet = bitstream.readBool()
21 | self.encryption_algorithm = bitstream.readBits(6)
22 | self.pts_adjustment = bitstream.readBits(33)
23 | self.cw_index = bitstream.readBits(8)
24 | self.tier = bitstream.readBits(12)
25 | self.splice_command_length = bitstream.readBits(12)
26 | self.splice_command_type = bitstream.readBits(8)
27 | if self.splice_command_type == SpliceInfoSection.SPLICE_NULL:
28 | self.splice_command = SpliceNull(bitstream)
29 | elif self.splice_command_type == SpliceInfoSection.SPLICE_SCHEDULE:
30 | self.splice_command = SpliceSchedule(bitstream)
31 | elif self.splice_command_type == SpliceInfoSection.SPLICE_INSERT:
32 | self.splice_command = SpliceInsert(bitstream)
33 | elif self.splice_command_type == SpliceInfoSection.TIME_SIGNAL:
34 | self.splice_command = TimeSignal(bitstream)
35 | elif self.splice_command_type == SpliceInfoSection.BANDWIDTH_RESERVATION:
36 | self.splice_command = BandwidthReservation(bitstream)
37 | elif self.splice_command_type == SpliceInfoSection.PRIVATE_COMMAND:
38 | self.splice_command = PrivateCommand(bitstream, self.splice_command_length)
39 | self.descriptor_loop_length = bitstream.readBits(16)
40 | descriptor_stream = bitstream.readBitStreamFromBytes(self.descriptor_loop_length)
41 | self.descriptors: list[Descriptor] = []
42 | while descriptor_stream:
43 | descriptor_tag = descriptor_stream.readBits(8)
44 | descriptor_stream.retainByte(descriptor_tag)
45 | if descriptor_tag == 0x00:
46 | self.descriptors.append(AvailDescriptor(descriptor_stream))
47 | elif descriptor_tag == 0x01:
48 | self.descriptors.append(DTMFDescriptor(descriptor_stream))
49 | elif descriptor_tag == 0x02:
50 | self.descriptors.append(SegmentationDescriptor(descriptor_stream))
51 | elif descriptor_tag == 0x03:
52 | self.descriptors.append(TimeDescriptor(descriptor_stream))
53 | elif descriptor_tag == 0x04:
54 | self.descriptors.append(AudioDescriptor(descriptor_tag))
55 |
56 | if self.encrypted_packet:
57 | self.E_CRC_32 = bitstream.readBits(32)
58 |
59 | class SpliceNull:
60 | def __init__(self, bitstream):
61 | pass
62 |
63 | class SpliceSchedule:
64 | def __init__(self, bitstream):
65 | self.splice_count = bitstream.readBits(8)
66 | self.events = [
67 | SpliceScheduleEvent(bitstream)
68 | for _ in range(self.splice_count)
69 | ]
70 |
71 | class SpliceScheduleEvent:
72 | def __init__(self, bitstream):
73 | self.splice_event_id = bitstream.readBits(32)
74 | self.splice_event_cancel_indicator = bitstream.readBool()
75 | bitstream.readBits(7)
76 | if not self.splice_event_cancel_indicator:
77 | self.out_of_network_indicator = bitstream.readBool()
78 | self.program_splice_flag = bitstream.readBool()
79 | self.duration_flag = bitstream.readBool()
80 | bitstream.readBits(5)
81 | if self.program_splice_flag:
82 | self.utc_splice_time = bitstream.readBits(32)
83 | else:
84 | self.component_count = bitstream.readBits(8)
85 | self.components = [
86 | SpliceScheduleEventComponent(bitstream)
87 | for _ in range(self.component_count)
88 | ]
89 | if self.duration_flag:
90 | self.break_duration = BreakDuration(bitstream)
91 | self.unique_program_id = bitstream.readBits(16)
92 | self.avail_num = bitstream.readBits(8)
93 | self.avails_expected = bitstream.readBits(8)
94 |
95 | class SpliceScheduleEventComponent:
96 | def __init__(self, bitstream):
97 | self.component_tag = bitstream.readBits(8)
98 | self.utc_splice_time = bitstream.readBits(32)
99 |
100 | class SpliceInsert:
101 | def __init__(self, bitstream):
102 | self.splice_event_id = bitstream.readBits(32)
103 | self.splice_event_cancel_indicator = bitstream.readBool()
104 | bitstream.readBits(7)
105 | if not self.splice_event_cancel_indicator:
106 | self.out_of_network_indicator = bitstream.readBool()
107 | self.program_splice_flag = bitstream.readBool()
108 | self.duration_flag = bitstream.readBool()
109 | self.splice_immediate_flag = bitstream.readBool()
110 | bitstream.readBits(4)
111 | if self.program_splice_flag and not self.splice_immediate_flag:
112 | self.splice_time = SpliceTime(bitstream)
113 | if not self.program_splice_flag:
114 | self.component_count = bitstream.readBits(8)
115 | self.components = [
116 | SpliceInsertComponent(bitstream, self.splice_immediate_flag)
117 | for _ in range(self.component_count)
118 | ]
119 | if self.duration_flag:
120 | self.break_duration = BreakDuration(bitstream)
121 | self.unique_program_id = bitstream.readBits(16)
122 | self.avail_num = bitstream.readBits(8)
123 | self.avails_expected = bitstream.readBits(8)
124 |
125 | class SpliceInsertComponent:
126 | def __init__(self, bitstream, splice_immediate_flag):
127 | self.component_tag = bitstream.readBits(8)
128 | if not splice_immediate_flag:
129 | self.splice_time = SpliceTime(bitstream)
130 |
131 | class TimeSignal:
132 | def __init__(self, bitstream):
133 | self.splice_time = SpliceTime(bitstream)
134 |
135 | class BandwidthReservation:
136 | def __init__(self, bitstream):
137 | pass
138 |
139 | class PrivateCommand:
140 | def __init__(self, bitstream, length):
141 | self.identifier = bitstream.readBits(32)
142 | self.private_byte = bytes([
143 | bitstream.readBits(8)
144 | for _ in range(length - 4)
145 | ])
146 |
147 | class BreakDuration:
148 | def __init__(self, bitstream):
149 | self.auto_return = bitstream.readBool()
150 | bitstream.readBits(6)
151 | self.duration = bitstream.readBits(33)
152 |
153 | class SpliceTime:
154 | def __init__(self, bitstream):
155 | self.time_specified_flag: bool = bitstream.readBool()
156 | self.pts_time: int | None
157 | if self.time_specified_flag:
158 | bitstream.readBits(6)
159 | self.pts_time = bitstream.readBits(33)
160 | else:
161 | bitstream.readBits(7)
162 |
163 | class Descriptor:
164 | def __init__(self, bitstream):
165 | self.descriptor_tag = bitstream.readBits(8)
166 | self.descriptor_length = bitstream.readBits(8)
167 | self.identifier = bitstream.readBits(32)
168 |
169 | class SpliceDescriptor(Descriptor):
170 | def __init__(self, bitstream):
171 | super().__init__(bitstream)
172 | self.private_byte = bytes([
173 | bitstream.readBits(8)
174 | for _ in range(self.descriptor_length - 4)
175 | ])
176 |
177 | class AvailDescriptor(Descriptor):
178 | def __init__(self, bitstream):
179 | super().__init__(bitstream)
180 | self.provider_avail_id = bitstream.readBits(8)
181 |
182 | class DTMFDescriptor(Descriptor):
183 | def __init__(self, bitstream):
184 | super().__init__(bitstream)
185 | self.preroll = bitstream.readBits(8)
186 | self.dtmf_count = bitstream.readBits(3)
187 | bitstream.readBits(5)
188 | self.DTMF_char = "".join([
189 | chr(bitstream.readBits(8))
190 | for _ in range(self.dtmf_count)
191 | ])
192 |
193 | class SegmentationDescriptor(Descriptor):
194 | NOT_INDICATED = 0x00
195 | CONTENT_IDENTIFICATION = 0x01
196 | PROGRAM_START = 0x10
197 | PROGRAM_END = 0x11
198 | PROGRAM_EARLY_TERMINATION = 0x12
199 | PROGRAM_BREAK_AWAY = 0x13
200 | PROGRAM_RESUMPTION = 0x14
201 | PROGRAM_RUNOVER_PLANNED = 0x15
202 | PROGRAM_RUNOVER_UNPLANNED = 0x16
203 | PROGRAM_OVERLAP_START = 0x17
204 | PROGRAM_BLACKOUT_OVERRIDE = 0x18
205 | PROGRAM_START_INPROGRESS = 0x19
206 | CHAPTER_START = 0x20
207 | CHAPTER_END = 0x21
208 | BREAK_START = 0x22
209 | BREAK_END = 0x23
210 | OPENING_CREDIT_START = 0x24
211 | OPENNIN_CREDIT_END = 0x25
212 | CLOSING_CREDIT_START = 0x26
213 | CLOSING_CREDIT_END = 0x27
214 | PROVIDER_ADVERTISEMENT_START = 0x30
215 | PROVIDER_ADVERTISEMENT_END = 0x31
216 | DISTRIBUTOR_ADVERTISEMENT_START = 0x32
217 | DISTRIBUTOR_ADVERTISEMENT_END = 0x33
218 | PROVIDER_PLACEMENT_OPPORTUNITY_START = 0x34
219 | PROVIDER_PLACEMENT_OPPORTUNITY_END = 0x35
220 | DISTRIBUTOR_PLACEMENT_OPPORTUNITY_START = 0x36
221 | DISTRIBUTOR_PLACEMENT_OPPORTUNITY_END = 0x37
222 | PROVIDER_OVERLAY_PLACEMENT_OPPORTUNITY_START = 0x38
223 | PROVIDER_OVERLAY_PLACEMENT_OPPORTUNITY_END = 0x39
224 | DISTRIBUTOR_OVERLAY_PLACEMENT_OPPORTUNITY_START = 0x3A
225 | DISTRIBUTOR_OVERLAY_PLACEMENT_OPPORTUNITY_END = 0x3B
226 | UNSCHEDULED_EVENT_START = 0x40
227 | UNSCHEDULED_EVENT_END = 0x41
228 | NETWORK_START = 0x50
229 | NETWORk_END = 0x51
230 |
231 | ADVERTISEMENT_BEGIN = set([
232 | PROVIDER_ADVERTISEMENT_START,
233 | DISTRIBUTOR_ADVERTISEMENT_START,
234 | PROVIDER_PLACEMENT_OPPORTUNITY_START,
235 | DISTRIBUTOR_PLACEMENT_OPPORTUNITY_START
236 | ])
237 | ADVERTISEMENT_END = set([
238 | PROVIDER_ADVERTISEMENT_END,
239 | DISTRIBUTOR_ADVERTISEMENT_END,
240 | PROVIDER_PLACEMENT_OPPORTUNITY_END,
241 | DISTRIBUTOR_PLACEMENT_OPPORTUNITY_END
242 | ])
243 |
244 | def __init__(self, bitstream):
245 | super().__init__(bitstream)
246 | self.segmentation_event_id = bitstream.readBits(32)
247 | self.segmentation_event_cancel_indicator = bitstream.readBool()
248 | bitstream.readBits(7)
249 | if not self.segmentation_event_cancel_indicator:
250 | self.program_segmentation_flag = bitstream.readBool()
251 | self.segmentation_duration_flag = bitstream.readBool()
252 | self.delivery_not_restricted_flag = bitstream.readBool()
253 | if not self.delivery_not_restricted_flag:
254 | self.web_delivery_allowed_flag = bitstream.readBool()
255 | self.no_regional_blackout_flag = bitstream.readBool()
256 | self.archive_allowed_flag = bitstream.readBool()
257 | self.device_restrictions = bitstream.readBits(2)
258 | else:
259 | bitstream.readBits(5)
260 | if not self.program_segmentation_flag:
261 | self.component_count = bitstream.readBits(8)
262 | self.components = [
263 | SegmentationDescriptorComponent(bitstream)
264 | for _ in range(self.component_count)
265 | ]
266 | if self.segmentation_duration_flag:
267 | self.segmentation_duration = bitstream.readBits(40)
268 | self.segmentation_upid_type = bitstream.readBits(8)
269 | self.segmentation_upid_length = bitstream.readBits(8)
270 | self.segmentation_upid = bitstream.readBits(self.segmentation_upid_length * 8).to_bytes(8, 'big')
271 | bitstream.readBits(self.segmentation_upid_length * 8)
272 | self.segmentation_type_id = bitstream.readBits(8)
273 | self.segment_num = bitstream.readBits(8)
274 | self.segments_expected = bitstream.readBits(8)
275 | if self.segmentation_type_id in [0x34, 0x36, 0x38, 0x3A]:
276 | self.sub_segment_num = bitstream.readBits(8)
277 | self.sub_segments_expected = bitstream.readBits(8)
278 |
279 | class SegmentationDescriptorComponent:
280 | def __init__(self, bitstream):
281 | self.component_tag = bitstream.readBits(8)
282 | bitstream.readBits(7)
283 | self.pts_offset = bitstream.readBits(33)
284 |
285 | class SegmentationUpid:
286 | def __init__(self, bitstream):
287 | pass
288 |
289 | class TimeDescriptor(Descriptor):
290 | def __init__(self, bitstream):
291 | super().__init__(bitstream)
292 | self.TAI_seconds = bitstream.readBits(48)
293 | self.TAI_ns = bitstream.readBits(32)
294 | self.UTC_offset = bitstream.readBits(16)
295 |
296 | class AudioDescriptor(Descriptor):
297 | def __init__(self, bitstream):
298 | super().__init__(bitstream)
299 | self.audio_count = bitstream.readBits(4)
300 | bitstream.readBits(4)
301 | self.components = [
302 | AudioDescriptorComponent(bitstream)
303 | for _ in range(self.audio_count)
304 | ]
305 |
306 | class AudioDescriptorComponent:
307 | def __init__(self, bitstream):
308 | self.component_tag = bitstream.readBits(8)
309 | self.ISO_code = bitstream.readBits(24)
310 | self.Bit_Stream_Mode = bitstream.readBits(3)
311 | self.Num_Channels = bitstream.readBits(4)
312 | self.Full_Srvc_Audio = bitstream.readBool()
313 |
--------------------------------------------------------------------------------
/pseudo.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | # Linux 環境において、事前に FFmpeg・FFprobe・QSVEncC・NVEncC・tsreadex がパスの通った場所にインストールされている前提で実装されている
4 |
5 | from typing import cast
6 |
7 | import asyncio
8 | from aiohttp import web
9 | from aiohttp_sse import sse_response
10 |
11 | import json
12 | import math
13 | from itertools import accumulate
14 |
15 | from biim.mpeg2ts import ts
16 | from biim.mpeg2ts.packetize import packetize_section, packetize_pes
17 | from biim.mpeg2ts.parser import SectionParser, PESParser
18 | from biim.mpeg2ts.pat import PATSection
19 | from biim.mpeg2ts.pmt import PMTSection
20 | from biim.mpeg2ts.pes import PES
21 |
22 | import argparse
23 | import os
24 | from datetime import datetime
25 | from pathlib import Path
26 |
27 | from pseudo_quality import getEncoderCommand
28 |
29 | async def keyframe_info(input: Path, targetduration: float) -> list[tuple[int, float]]:
30 | options = ['-i', f'{input}', '-select_streams', 'v:0', '-show_packets', '-show_entries', 'packet=pts,dts,flags,pos', '-of', 'json']
31 | prober = await asyncio.subprocess.create_subprocess_exec('ffprobe', *options, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL)
32 | raw_frames = [('K' in data['flags'], int(data['pos']), int(data['dts'])) for data in json.loads((await cast(asyncio.StreamReader, prober.stdout).read()).decode('utf-8'))['packets']]
33 | filtered_frames = [(pos, dts) for (key, pos, dts) in raw_frames if key] + ([(raw_frames[-1][1], raw_frames[-1][2])] if raw_frames[-1][0] else [])
34 | segments = [(pos, (end - begin) / ts.HZ) for (pos, begin), (_, end) in zip(filtered_frames[0:], filtered_frames[1:])]
35 | merged = [segments[0]]
36 | for segment in segments[1:]:
37 | if merged[-1][1] >= targetduration:
38 | merged.append((segment))
39 | else:
40 | merged[-1] = (merged[-1][0], merged[-1][1] + segment[1])
41 | return merged
42 |
43 | async def main():
44 | loop = asyncio.get_running_loop()
45 | parser = argparse.ArgumentParser(description=('biim: HLS Pseudo VOD In-Memroy Origin'))
46 |
47 | parser.add_argument('-i', '--input', type=Path, required=True)
48 | parser.add_argument('-t', '--targetduration', type=float, nargs='?', default=2.5)
49 | parser.add_argument('-p', '--port', type=int, nargs='?', default=8080)
50 | parser.add_argument('-e', '--encoder', type=str, nargs='?', default='FFmpeg')
51 | parser.add_argument('-q', '--quality', type=str, nargs='?', default='1080p')
52 |
53 | args = parser.parse_args()
54 | input_path: Path = args.input
55 |
56 | # setup pseudo playlist/segment
57 | print('calculating keyframe info...')
58 | segments = await keyframe_info(input_path, args.targetduration)
59 | offsets = [0] + list(accumulate(duration for _, duration in segments))
60 | print('calculating keyframe info... done')
61 | num_of_segments = len(segments)
62 | target_duration = math.ceil(max(duration for _, duration in segments))
63 | virtual_cache: str = f'init-{datetime.now().strftime("%Y%m%d%H%M%S")}'
64 | virtual_segments: list[asyncio.Future[bytes | bytearray | memoryview | None]] = []
65 | processing: list[bool] = []
66 | process_queue: asyncio.Queue[int] = asyncio.Queue()
67 | buffer_index: tuple[int, int] = (0, 0)
68 | buffer_notify: asyncio.Future[None] = asyncio.Future()
69 |
70 | async def index(request):
71 | return web.FileResponse('pseudo.html')
72 |
73 | async def m3u8(cache: str, s: int):
74 | virutal_playlist_header = ''
75 | virutal_playlist_header += f'#EXTM3U\n'
76 | virutal_playlist_header += f'#EXT-X-VERSION:6\n'
77 | virutal_playlist_header += f'#EXT-X-TARGETDURATION:{target_duration}\n'
78 | virutal_playlist_header += f'#EXT-X-PLAYLIST-TYPE:VOD\n'
79 | virtual_playlist_body = ''
80 | for seq, (_, duration) in enumerate(segments):
81 | virtual_playlist_body += f"#EXTINF:{duration:.06f}\n"
82 | virtual_playlist_body += f"segment?seq={seq}&_={cache}\n"
83 | virtual_playlist_body += "\n"
84 | virtual_playlist_tail = '#EXT-X-ENDLIST\n'
85 | return virutal_playlist_header + virtual_playlist_body + virtual_playlist_tail
86 |
87 | async def playlist(request: web.Request) -> web.Response:
88 | nonlocal virtual_cache
89 | version = request.query['_'] if '_' in request.query else f'init-{datetime.now().strftime("%Y%m%d%H%M%S")}'
90 | t = float(request.query['t']) if 't' in request.query else 0
91 | seq = 0
92 | for segment in segments:
93 | if t < segment[1]: break
94 | t -= segment[1]
95 | seq += 1
96 |
97 | if not virtual_segments[seq].done() and not processing[seq]:
98 | virtual_cache = version
99 |
100 | result = await asyncio.shield(m3u8(virtual_cache, seq))
101 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, text=result, content_type="application/x-mpegURL")
102 |
103 | async def segment(request: web.Request) -> web.Response:
104 | nonlocal buffer_index
105 | seq = request.query['seq'] if 'seq' in request.query else None
106 |
107 | if seq is None:
108 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type="video/mp2t")
109 |
110 | seq = int(seq)
111 | if seq < 0 or seq >= len(virtual_segments):
112 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type="video/mp2t")
113 |
114 | if not virtual_segments[seq].done() and not processing[seq]:
115 | await process_queue.put(seq)
116 | await process_queue.join()
117 |
118 | body = await asyncio.shield(virtual_segments[seq])
119 |
120 | if body is None:
121 | return await playlist(request)
122 |
123 | for prev in range(buffer_index[0], seq - 10):
124 | virtual_segments[prev] = asyncio.Future()
125 | buffer_index = (prev + 1, buffer_index[1])
126 | if not buffer_notify.done(): buffer_notify.set_result(None)
127 |
128 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=3600'}, body=body, content_type="video/mp2t")
129 |
130 | async def buffer(request: web.Request) -> web.StreamResponse:
131 | nonlocal buffer_notify
132 | async with sse_response(request) as resp:
133 | while resp.is_connected():
134 | time_dict = {"begin": offsets[buffer_index[0]], "end": offsets[buffer_index[1]]}
135 | data = json.dumps(time_dict, indent=2)
136 | await resp.send(data)
137 | buffer_notify = asyncio.Future()
138 | try:
139 | await asyncio.wait_for(asyncio.shield(buffer_notify), timeout=1.0)
140 | except asyncio.TimeoutError:
141 | pass
142 | return resp
143 |
144 | # setup aiohttp
145 | app = web.Application()
146 | app.add_routes([
147 | web.get('/', index),
148 | web.get('/playlist.m3u8', playlist),
149 | web.get('/segment', segment),
150 | web.get('/buffer', buffer)
151 | ])
152 | runner = web.AppRunner(app)
153 | await runner.setup()
154 | await loop.create_server(cast(web.Server, runner.server), '0.0.0.0', args.port)
155 |
156 | virtual_segments = [asyncio.Future[bytes | bytearray | memoryview | None]() for _ in range(num_of_segments)]
157 | processing = [False for _ in range(num_of_segments)]
158 | encoder: asyncio.subprocess.Process | None = None
159 |
160 | await process_queue.put(0)
161 | while True:
162 | seq = await process_queue.get()
163 | for idx in range(len(processing)): processing[idx] = False
164 | processing[seq] = True
165 | for future in virtual_segments:
166 | if not future.done(): future.set_result(None)
167 | virtual_segments = [asyncio.Future[bytes | bytearray | memoryview | None]() for _ in range(num_of_segments)]
168 | pos, _ = segments[seq]
169 | offset = sum((duration for _, duration in segments[:seq]), 0)
170 | buffer_index = (seq, seq)
171 | if not buffer_notify.done(): buffer_notify.set_result(None)
172 | process_queue.task_done()
173 |
174 | encoder_command = getEncoderCommand(args.encoder, args.quality, int(offset))
175 | print(encoder_command)
176 | if encoder:
177 | if file:
178 | file.seek(0, os.SEEK_END)
179 | file.close()
180 | await encoder.communicate()
181 | await encoder.wait()
182 | file = open(input_path, "rb")
183 | file.seek(pos) # fd がエンコーダープロセスと共通なことを利用して、ファイルポインタを移動させる
184 | encoder = await asyncio.subprocess.create_subprocess_shell(" ".join(encoder_command), stdin=file, stdout=asyncio.subprocess.PIPE)
185 | reader = cast(asyncio.StreamReader, encoder.stdout)
186 |
187 | PAT_Parser: SectionParser[PATSection] = SectionParser(PATSection)
188 | PMT_Parser: SectionParser[PMTSection] = SectionParser(PMTSection)
189 | Video_Praser: PESParser[PES] = PESParser(PES)
190 | Audio_Praser: PESParser[PES] = PESParser(PES)
191 | LATEST_PAT: PATSection | None = None
192 | LATEST_PMT: PMTSection | None = None
193 | PAT_CC: int = 0
194 | PMT_PID: int | None = None
195 | PMT_CC: int = 0
196 | VIDEO_PID: int | None = None
197 | VIDEO_CC: int = 0
198 | AUDIO_PID: int | None = None
199 | AUDIO_CC: int = 0
200 | candidate = bytearray()
201 |
202 | while process_queue.empty():
203 | if seq >= len(segments): break
204 |
205 | isEOF = False
206 | while True:
207 | try:
208 | sync_byte = await reader.readexactly(1)
209 | if sync_byte == ts.SYNC_BYTE:
210 | break
211 | elif sync_byte == b'':
212 | isEOF = True
213 | break
214 | except asyncio.IncompleteReadError:
215 | isEOF = True
216 | break
217 | if isEOF:
218 | break
219 |
220 | packet = None
221 | try:
222 | packet = ts.SYNC_BYTE + await reader.readexactly(ts.PACKET_SIZE - 1)
223 | except asyncio.IncompleteReadError:
224 | break
225 |
226 | PID = ts.pid(packet)
227 | if PID == 0x00:
228 | PAT_Parser.push(packet)
229 | for PAT in PAT_Parser:
230 | if PAT.CRC32() != 0: continue
231 | LATEST_PAT = PAT
232 |
233 | for program_number, program_map_PID in PAT:
234 | if program_number == 0: continue
235 | PMT_PID = program_map_PID
236 |
237 | for packet in packetize_section(PAT, False, False, 0, 0, PAT_CC):
238 | candidate += packet
239 | PAT_CC = (PAT_CC + 1) & 0x0F
240 |
241 | elif PID == PMT_PID:
242 | PMT_Parser.push(packet)
243 | for PMT in PMT_Parser:
244 | if PMT.CRC32() != 0: continue
245 | LATEST_PMT = PMT
246 |
247 | for stream_type, elementary_PID, _ in PMT:
248 | if stream_type == 0x1b: # H.264
249 | VIDEO_PID = elementary_PID
250 | elif stream_type == 0x24: # H.265
251 | VIDEO_PID = elementary_PID
252 | elif stream_type == 0x0F: # AAC
253 | AUDIO_PID = elementary_PID
254 |
255 | for packet in packetize_section(PMT, False, False, cast(int, PMT_PID), 0, PMT_CC):
256 | candidate += packet
257 | PMT_CC = (PMT_CC + 1) & 0x0F
258 |
259 | elif PID == VIDEO_PID:
260 | Video_Praser.push(packet)
261 | for VIDEO in Video_Praser:
262 | timestamp = cast(int, VIDEO.dts() or VIDEO.pts()) / ts.HZ
263 |
264 | if timestamp >= offset + segments[seq][1]:
265 | virtual_segments[seq].set_result(candidate)
266 | processing[seq] = False
267 | buffer_index = (buffer_index[0], seq + 1)
268 | if not buffer_notify.done(): buffer_notify.set_result(None)
269 | offset += segments[seq][1]
270 | seq += 1
271 | candidate = bytearray()
272 | if seq >= len(segments):
273 | break
274 | processing[seq] = True
275 |
276 | for packet in packetize_section(cast(PATSection, LATEST_PAT), False, False, 0, 0, PAT_CC):
277 | candidate += packet
278 | PAT_CC = (PAT_CC + 1) & 0x0F
279 | for packet in packetize_section(cast(PMTSection, LATEST_PMT), False, False, cast(int, PMT_PID), 0, PMT_CC):
280 | candidate += packet
281 | PMT_CC = (PMT_CC + 1) & 0x0F
282 |
283 | for packet in packetize_pes(VIDEO, False, False, cast(int, VIDEO_PID), 0, VIDEO_CC):
284 | candidate += packet
285 | VIDEO_CC = (VIDEO_CC + 1) & 0x0F
286 |
287 | elif PID == AUDIO_PID:
288 | Audio_Praser.push(packet)
289 | for AUDIO in Audio_Praser:
290 | for packet in packetize_pes(AUDIO, False, False, cast(int, AUDIO_PID), 0, AUDIO_CC):
291 | candidate += packet
292 | AUDIO_CC = (AUDIO_CC + 1) & 0x0F
293 |
294 | else:
295 | candidate += packet
296 |
297 | if __name__ == '__main__':
298 | asyncio.run(main())
299 |
--------------------------------------------------------------------------------
/biim/mp4/box.py:
--------------------------------------------------------------------------------
1 | from typing import cast
2 |
3 | composition_matrix = bytes([
4 | 0x00, 0x01, 0x00, 0x00,
5 | 0x00, 0x00, 0x00, 0x00,
6 | 0x00, 0x00, 0x00, 0x00,
7 | 0x00, 0x00, 0x00, 0x00,
8 | 0x00, 0x01, 0x00, 0x00,
9 | 0x00, 0x00, 0x00, 0x00,
10 | 0x00, 0x00, 0x00, 0x00,
11 | 0x00, 0x00, 0x00, 0x00,
12 | 0x40, 0x00, 0x00, 0x00,
13 | ])
14 |
15 | def box(fourcc: str, data: list[bytes | bytearray | memoryview] | bytes | bytearray | memoryview = b'') -> bytes:
16 | total = sum(map(len, data)) if type(data) is list else len(data)
17 | return (8 + total).to_bytes(4, byteorder='big') + fourcc.encode('ascii') + (b''.join(data) if type(data) is list else cast(bytes | bytearray | memoryview, data))
18 |
19 | def fullbox(fourcc: str, version: int, flags: int, data: list[bytes | bytearray | memoryview] | bytes | bytearray | memoryview = b'') -> bytes:
20 | return box(fourcc, [version.to_bytes(1, byteorder='big'), flags.to_bytes(3, byteorder='big'), (b''.join(data) if type(data) is list else cast(bytes | bytearray | memoryview, data))])
21 |
22 | def ftyp() -> bytes:
23 | return box('ftyp', [
24 | 'isom'.encode('ascii'), # major_brand: isom
25 | (1).to_bytes(4, byteorder='big'), # minor_version: 0x01
26 | 'isom'.encode('ascii'),
27 | 'avc1'.encode('ascii')
28 | ])
29 |
30 | def moov(mvhd: bytes, mvex: bytes, trak: bytes | list[bytes]) -> bytes:
31 | return box('moov', [
32 | mvhd,
33 | mvex,
34 | b''.join(trak) if type(trak) is list else cast(bytes, trak)
35 | ])
36 |
37 | def mvhd(timescale: int) -> bytes:
38 | return fullbox('mvhd', 0, 0, [
39 | (0).to_bytes(4, byteorder='big'), # creation_time
40 | (0).to_bytes(4, byteorder='big'), # modification_time
41 | timescale.to_bytes(4, byteorder='big'), # timescale
42 | (0).to_bytes(4, byteorder='big'), # duration
43 | b'\x00\x01\x00\x00', # Preferred rate: 1.0
44 | b'\x01\x00\x00\x00', # PreferredVolume(1.0, 2bytes) + reserved(2bytes)
45 | (0).to_bytes(4 * 2, byteorder='big'), # reserved: 4 + 4 bytes
46 | composition_matrix, # composition_matrix
47 | (0).to_bytes(4 * 6, byteorder='big'), # reserved: 6 * 4 bytes
48 | b'\xFF\xFF\xFF\xFF' # next_track_ID
49 | ])
50 |
51 | def trak(tkhd: bytes, mdia: bytes) -> bytes:
52 | return box('trak', tkhd + mdia)
53 |
54 | def tkhd(trackId: int, width: int, height: int) -> bytes:
55 | return fullbox('tkhd', 0, 0x000007, [
56 | (0).to_bytes(4, byteorder='big'), # creation_time
57 | (0).to_bytes(4, byteorder='big'), # modification_time
58 | trackId.to_bytes(4, byteorder='big'), # trackId
59 | (0).to_bytes(4, byteorder='big'), # reserved: 4 byte
60 | (0).to_bytes(4, byteorder='big'), # duration
61 | (0).to_bytes(4 * 2, byteorder='big'), # reserved: 4 + 4 bytes
62 | (0).to_bytes(4 * 2, byteorder='big'), # layer(2bytes) + alternate_group(2bytes), volume(2bytes) + reserved(2bytes)
63 | composition_matrix, # composition_matrix
64 | (width).to_bytes(2, byteorder='big') + b'\x00\x00', # width
65 | (height).to_bytes(2, byteorder='big') + b'\x00\x00', # height
66 | ])
67 |
68 | def mdia(mdhd: bytes, hdlr: bytes, minf: bytes) -> bytes:
69 | return box('mdia', [mdhd, hdlr, minf])
70 |
71 | def mdhd(timescale: int) -> bytes:
72 | return fullbox('mdhd', 0, 0, [
73 | (0).to_bytes(4, byteorder='big'), # creation_time
74 | (0).to_bytes(4, byteorder='big'), # modification_time
75 | timescale.to_bytes(4, byteorder='big'), # timescale
76 | (0).to_bytes(4, byteorder='big'), # duration
77 | b'\x55\xC4' + (0).to_bytes(2, byteorder='big'), # language: und (undetermined), pre_defined = 0
78 | ])
79 |
80 | def hdlr(handler_type: str, handler_name: str) -> bytes:
81 | return fullbox('hdlr', 0, 0, [
82 | (0).to_bytes(4, byteorder='big'), # pre_defined
83 | handler_type.encode('ascii'), # handler_type
84 | (3 * 4).to_bytes(3 * 4, byteorder='big'), # reserved: 3 * 4 bytes
85 | handler_name.encode('ascii') + b'\x00' # handler_name
86 | ])
87 |
88 | def minf(xmhd: bytes, dinf: bytes, stbl: bytes) -> bytes:
89 | return box('minf', [
90 | xmhd or nmhd(),
91 | dinf,
92 | stbl
93 | ])
94 |
95 | def nmhd() -> bytes:
96 | return fullbox('nmhd', 0, 0)
97 |
98 | def vmhd() -> bytes:
99 | return fullbox('vmhd', 0, 1, [
100 | (0).to_bytes(2, byteorder='big'), # graphicsmode: 2 bytes
101 | (0).to_bytes(6, byteorder='big'), # opcolor: 3 * 2 bytes
102 | ])
103 |
104 | def smhd() -> bytes:
105 | return fullbox('smhd', 0, 1, [
106 | (0).to_bytes(2, byteorder='big'), (0).to_bytes(2, byteorder='big'), # balance(2) + reserved(2)
107 | ])
108 |
109 | def dinf() -> bytes:
110 | return box('dinf',
111 | fullbox('dref', 0, 0, [(1).to_bytes(4, byteorder='big'), fullbox('url ', 0, 1)])
112 | )
113 |
114 | def stbl(stsd: bytes) -> bytes:
115 | return box('stbl', [
116 | stsd,
117 | fullbox('stts', 0, 0, (0).to_bytes(4, byteorder='big')),
118 | fullbox('stsc', 0, 0, (0).to_bytes(4, byteorder='big')),
119 | fullbox('stsz', 0, 0, (0).to_bytes(8, byteorder='big')),
120 | fullbox('stco', 0, 0, (0).to_bytes(4, byteorder='big')),
121 | ])
122 |
123 | def stsd(codec: bytes) -> bytes:
124 | return fullbox('stsd', 0, 1, [
125 | (1).to_bytes(4, byteorder='big'),
126 | codec
127 | ])
128 |
129 | def mp4a(config: bytes | bytearray | memoryview, channelCount: int, sampleRate: int) -> bytes:
130 | return box('mp4a', [
131 | (0).to_bytes(4, byteorder='big'), # reserved(4)
132 | (0).to_bytes(2, byteorder='big'), (1).to_bytes(2, byteorder='big'), # reserved(2) + data_reference_index(2)
133 | (0).to_bytes(4 * 2, byteorder='big'), # reserved(8)
134 | (channelCount).to_bytes(2, byteorder='big'), (0x10).to_bytes(2, byteorder='big'), # channelCount(2) + sampleSize(2)
135 | (0).to_bytes(4, byteorder='big'), # reserved(4)
136 | (sampleRate).to_bytes(2, byteorder='big'), (0x00).to_bytes(2, byteorder='big'), # sampleRate(2) + sampleSize(2)
137 | esds(config, bytes([ 0x06, 0x01, 0x02 ])), # with GASpecificConfig
138 | ])
139 |
140 | def esds(config: bytes | bytearray | memoryview, descriptor: bytes = b'') -> bytes:
141 | return fullbox('esds', 0, 0, [
142 | (0x03).to_bytes(1, byteorder='big'), # descriptor_type
143 | (0x17 + len(config)).to_bytes(1, byteorder='big'), # length
144 | (0x01).to_bytes(2, byteorder='big'), # es_id
145 | (0).to_bytes(1, byteorder='big'), # stream_priority
146 | (0x04).to_bytes(1, byteorder='big'), # descriptor_type
147 | (0x0F + len(config)).to_bytes(1, byteorder='big'), # length
148 | (0x40).to_bytes(1, byteorder='big'), # codec: mpeg4_audio
149 | (0x15).to_bytes(1, byteorder='big'), # stream_type: Audio
150 | (0).to_bytes(3, byteorder='big'), # buffer_size
151 | (0).to_bytes(4, byteorder='big'), # maxBitrate
152 | (0).to_bytes(4, byteorder='big'), # avgBitrate
153 | (0x05).to_bytes(1, byteorder='big'), # descriptor_type
154 | (len(config)).to_bytes(1, byteorder='big'), # length
155 | config,
156 | descriptor,
157 | ])
158 |
159 | def avc1(config: bytes | bytearray | memoryview, width: int, height: int) -> bytes:
160 | return box('avc1', [
161 | (0).to_bytes(4, byteorder='big'), # rereserved(4)
162 | (0).to_bytes(2, byteorder='big'), (1).to_bytes(2, byteorder='big'), # reserved(2) + data_reference_index(2)
163 | (0).to_bytes(2, byteorder='big'), (0).to_bytes(2, byteorder='big'), # pre_defined(2) + reserved(2)
164 | (0).to_bytes(3 * 4, byteorder='big'), # pre_defined: 3 * 4 bytes
165 | (width).to_bytes(2, byteorder='big'), (height).to_bytes(2, byteorder='big'), # width 2bytes, height: 2 bytes
166 | (0x48).to_bytes(2, byteorder='big'), (0).to_bytes(2, byteorder='big'), # horizresolution: 4 bytes divide 2bytes
167 | (0x48).to_bytes(2, byteorder='big'), (0).to_bytes(2, byteorder='big'), # vertresolution: 4 bytes divide 2bytes
168 | (0).to_bytes(4, byteorder='big'), # rereserved(4)
169 | (1).to_bytes(2, byteorder='big'), # frame_count
170 | (0).to_bytes(32, byteorder='big'), # compressorname (strlen, 1byte, total 32bytes)
171 | (0x18).to_bytes(2, byteorder='big'), b'\xFF\xFF', # depth, pre_defined = -1
172 | box('avcC', config)
173 | ])
174 |
175 | def hvc1(config: bytes | bytearray | memoryview, width: int, height: int) -> bytes:
176 | return box('hvc1', [
177 | (0).to_bytes(4, byteorder='big'), # rereserved(4)
178 | (0).to_bytes(2, byteorder='big'), (1).to_bytes(2, byteorder='big'), # reserved(2) + data_reference_index(2)
179 | (0).to_bytes(2, byteorder='big'), (0).to_bytes(2, byteorder='big'), # pre_defined(2) + reserved(2)
180 | (0).to_bytes(3 * 4, byteorder='big'), # pre_defined: 3 * 4 bytes
181 | (width).to_bytes(2, byteorder='big'), (height).to_bytes(2, byteorder='big'), # width 2bytes, height: 2 bytes
182 | (0x48).to_bytes(2, byteorder='big'), (0).to_bytes(2, byteorder='big'), # horizresolution: 4 bytes divide 2bytes
183 | (0x48).to_bytes(2, byteorder='big'), (0).to_bytes(2, byteorder='big'), # vertresolution: 4 bytes divide 2bytes
184 | (0).to_bytes(4, byteorder='big'), # rereserved(4)
185 | (1).to_bytes(2, byteorder='big'), # frame_count
186 | (0).to_bytes(32, byteorder='big'), # compressorname (strlen, 1byte, total 32bytes)
187 | (0x18).to_bytes(2, byteorder='big'), b'\xFF\xFF', # depth, pre_defined = -1
188 | box('hvcC', config)
189 | ])
190 |
191 | def wvtt() -> bytes:
192 | return box('wvtt', [
193 | (0).to_bytes(6, byteorder='big') + # ???
194 | (1).to_bytes(2, byteorder='big') + # dataReferenceIndex
195 | vttC()
196 | ])
197 |
198 | def vttC() -> bytes:
199 | return box('vttC', 'WEBVTT\n'.encode('ascii'))
200 |
201 | def mvex(trex: bytes | list[bytes]) -> bytes:
202 | return box('mvex', b''.join(trex) if type(trex) is list else cast(bytes, trex))
203 |
204 | def trex(trackId: int) -> bytes:
205 | return fullbox('trex', 0, 0, [
206 | trackId.to_bytes(4, byteorder='big'), # trackId
207 | (1).to_bytes(4, byteorder='big'), # default_sample_description_index
208 | (0).to_bytes(4, byteorder='big'), # default_sample_duration
209 | (0).to_bytes(4, byteorder='big'), # default_sample_size
210 | b'\x00\x01\x00\x01' # default_sample_flags
211 | ])
212 |
213 | def moof(sequence_number: int, fragments: list[tuple[int, int, int, int, list[tuple[int, int, bool, int]]]]) -> bytes:
214 | moofSize = len(
215 | box('moof', [
216 | mfhd(sequence_number),
217 | b''.join([traf(trackId, duration, baseMediaDecodeTime, offset, samples) for trackId, duration, baseMediaDecodeTime, offset, samples in fragments])
218 | ])
219 | )
220 | return box('moof', [
221 | mfhd(sequence_number),
222 | b''.join([traf(trackId, duration, baseMediaDecodeTime, moofSize + 8 + offset, samples) for trackId, duration, baseMediaDecodeTime, offset, samples in fragments])
223 | ])
224 |
225 | def mfhd(sequence_number: int) -> bytes:
226 | return fullbox('mfhd', 0, 0, [
227 | (sequence_number).to_bytes(4, byteorder='big')
228 | ])
229 |
230 | def traf(trackId: int, duration: int, baseMediaDecodeTime: int, offset: int, samples: list[tuple[int, int, bool, int]]) -> bytes:
231 | return box('traf', [
232 | tfhd(trackId, duration),
233 | tfdt(baseMediaDecodeTime),
234 | trun(offset, samples),
235 | ])
236 |
237 | def tfhd(trackId: int, duraiton: int) -> bytes:
238 | return fullbox('tfhd', 0, 8, [
239 | (trackId).to_bytes(4, byteorder='big'),
240 | (duraiton).to_bytes(4, byteorder='big')
241 | ])
242 |
243 | def tfdt(baseMediaDecodeTime: int) -> bytes:
244 | return fullbox('tfdt', 1, 0, baseMediaDecodeTime.to_bytes(8, byteorder='big'))
245 |
246 | def trun(offset: int, samples: list[tuple[int, int, bool, int]]) -> bytes:
247 | return fullbox('trun', 0, 0x000F01, [
248 | (len(samples)).to_bytes(4, byteorder='big'),
249 | (offset).to_bytes(4, byteorder='big'),
250 | b''.join([
251 | b''.join([
252 | (duration).to_bytes(4, byteorder='big'),
253 | (size).to_bytes(4, byteorder='big'),
254 | (2 if isKeyframe else 1).to_bytes(1, byteorder='big'),
255 | (((1 if isKeyframe else 0) << 6) | ((0 if isKeyframe else 1) << 0)).to_bytes(1, byteorder='big'),
256 | (0).to_bytes(2, byteorder='big'),
257 | (compositionTimeOffset).to_bytes(4, byteorder='big'),
258 | ]) for size, duration, isKeyframe, compositionTimeOffset in samples
259 | ])
260 | ])
261 |
262 | def mdat(data: bytes | bytearray | memoryview) -> bytes:
263 | return box('mdat', data)
264 |
265 | def emsg(timescale: int, presentationTime: int, duration: int | None, schemeIdUri: str, content: bytes | bytearray | memoryview) -> bytes:
266 | return fullbox('emsg', 1, 0, [
267 | (timescale).to_bytes(4, byteorder='big'),
268 | (presentationTime).to_bytes(8, byteorder='big'),
269 | (duration if duration is not None else 0xFFFFFFFF).to_bytes(4, byteorder='big'),
270 | (0).to_bytes(4, byteorder='big'), # id
271 | (schemeIdUri).encode('ascii') + b'\x00',
272 | b'\x00', # value
273 | content
274 | ])
275 |
--------------------------------------------------------------------------------
/biim/variant/handler.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from aiohttp import web
3 |
4 | from abc import ABC
5 | from typing import cast
6 | from collections import deque
7 | from datetime import datetime, timezone, timedelta
8 |
9 | from biim.hls.m3u8 import M3U8
10 | from biim.mpeg2ts import ts
11 | from biim.mpeg2ts.scte import SpliceInfoSection, SpliceInsert, TimeSignal, SegmentationDescriptor
12 |
13 | class VariantHandler(ABC):
14 |
15 | def __init__(self, target_duration: int, part_target: float, content_type: str, window_size: int | None = None, has_init: bool = False, has_video: bool = True, has_audio: bool = True):
16 | self.target_duration = target_duration
17 | self.part_target = part_target
18 | self.segment_timestamp: int | None = None
19 | self.part_timestamp: int | None = None
20 |
21 | # M3U8
22 | self.m3u8 = M3U8(target_duration=target_duration, part_target=part_target, window_size=window_size, has_init=has_init)
23 | self.init = asyncio.Future[bytes | bytearray | memoryview]() if has_init else None
24 | self.content_type = content_type
25 | self.has_video = has_video
26 | self.has_audio = has_audio
27 | self.video_codec = asyncio.Future[str]()
28 | self.audio_codec = asyncio.Future[str]()
29 | # PCR
30 | self.latest_pcr_value: int | None = None
31 | self.latest_pcr_datetime: datetime | None = None
32 | self.latest_pcr_monotonic_timestamp_90khz: int = 0
33 | # SCTE35
34 | self.scte35_out_queue: deque[tuple[str, datetime, datetime | None, dict]] = deque()
35 | self.scte35_in_queue: deque[tuple[str, datetime]] = deque()
36 | # Bitrate
37 | self.bitrate = asyncio.Future[int]()
38 |
39 | async def playlist(self, request: web.Request) -> web.Response:
40 | msn = request.query['_HLS_msn'] if '_HLS_msn' in request.query else None
41 | part = request.query['_HLS_part'] if '_HLS_part' in request.query else None
42 | skip = request.query['_HLS_skip'] == 'YES' if '_HLS_skip' in request.query else False
43 |
44 | if msn is None and part is None:
45 | future = self.m3u8.plain()
46 | if future is None:
47 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type="application/x-mpegURL")
48 |
49 | result = await future
50 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, text=result, content_type="application/x-mpegURL")
51 | else:
52 | if msn is None:
53 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type="application/x-mpegURL")
54 | msn = int(msn)
55 | if part is None: part = 0
56 | part = int(part)
57 | future = self.m3u8.blocking(msn, part, skip)
58 | if future is None:
59 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type="application/x-mpegURL")
60 |
61 | result = await future
62 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=36000'}, text=result, content_type="application/x-mpegURL")
63 |
64 | async def segment(self, request: web.Request) -> web.Response | web.StreamResponse:
65 | msn = request.query['msn'] if 'msn' in request.query else None
66 |
67 | if msn is None:
68 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type=self.content_type)
69 | msn = int(msn)
70 | queue = await self.m3u8.segment(msn)
71 | if queue is None:
72 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type=self.content_type)
73 |
74 | response = web.StreamResponse(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=36000', 'Content-Type': self.content_type}, status=200)
75 | await response.prepare(request)
76 |
77 | while True:
78 | stream = await queue.get()
79 | if stream == None : break
80 | await response.write(stream)
81 |
82 | await response.write_eof()
83 | return response
84 |
85 | async def partial(self, request: web.Request) -> web.Response | web.StreamResponse:
86 | msn = request.query['msn'] if 'msn' in request.query else None
87 | part = request.query['part'] if 'part' in request.query else None
88 |
89 | if msn is None:
90 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type=self.content_type)
91 | msn = int(msn)
92 | if part is None:
93 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type=self.content_type)
94 | part = int(part)
95 | queue = await self.m3u8.partial(msn, part)
96 | if queue is None:
97 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type=self.content_type)
98 |
99 | response = web.StreamResponse(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=36000', 'Content-Type': self.content_type}, status=200)
100 | await response.prepare(request)
101 |
102 | while True:
103 | stream = await queue.get()
104 | if stream == None : break
105 | await response.write(stream)
106 |
107 | await response.write_eof()
108 | return response
109 |
110 | async def initialization(self, _: web.Request) -> web.Response:
111 | if self.init is None:
112 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type=self.content_type)
113 |
114 | body = await asyncio.shield(self.init)
115 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=36000'}, body=body, content_type=self.content_type)
116 |
117 | async def bandwidth(self) -> int:
118 | return await self.m3u8.bandwidth()
119 |
120 | async def codec(self) -> str:
121 | if self.has_video and self.has_audio:
122 | return f'{await self.video_codec},{await self.audio_codec}'
123 | elif self.has_video:
124 | return f'{await self.video_codec}'
125 | elif self.has_audio:
126 | return f'{await self.audio_codec}'
127 | else:
128 | return ''
129 |
130 | def set_renditions(self, renditions: list[str]):
131 | self.m3u8.set_renditions(renditions)
132 |
133 | def program_date_time(self, pts: int | None) -> datetime | None:
134 | if self.latest_pcr_value is None or self.latest_pcr_datetime is None or pts is None: return None
135 | return self.latest_pcr_datetime + timedelta(seconds=(((pts - self.latest_pcr_value + ts.PCR_CYCLE) % ts.PCR_CYCLE) / ts.HZ))
136 |
137 | def timestamp(self, pts: int | None) -> int | None:
138 | if self.latest_pcr_value is None or pts is None: return None
139 | return ((pts - self.latest_pcr_value + ts.PCR_CYCLE) % ts.PCR_CYCLE) + self.latest_pcr_monotonic_timestamp_90khz
140 |
141 | def update(self, new_segment: bool | None, timestamp: int, program_date_time: datetime) -> bool:
142 | # SCTE35
143 | if new_segment:
144 | while self.scte35_out_queue:
145 | if self.scte35_out_queue[0][1] <= program_date_time:
146 | id, _, end_date, attributes = self.scte35_out_queue.popleft()
147 | self.m3u8.open(id, program_date_time, end_date, **attributes) # SCTE-35 の OUT を セグメント にそろえてる
148 | else: break
149 | while self.scte35_in_queue:
150 | if self.scte35_in_queue[0][1] <= program_date_time:
151 | id, _ = self.scte35_in_queue.popleft()
152 | self.m3u8.close(id, program_date_time) # SCTE-35 の IN を セグメント にそろえてる
153 | else: break
154 | # M3U8
155 | if new_segment or (new_segment is None and (self.segment_timestamp is None or (timestamp - self.segment_timestamp) >= self.target_duration * ts.HZ)):
156 | if self.part_timestamp is not None:
157 | part_diff = timestamp - self.part_timestamp
158 | if self.part_target * ts.HZ < part_diff:
159 | self.part_timestamp = int(timestamp - max(0, part_diff - self.part_target * ts.HZ))
160 | self.m3u8.continuousPartial(self.part_timestamp, False)
161 | self.part_timestamp = timestamp
162 | self.segment_timestamp = timestamp
163 | self.m3u8.continuousSegment(self.part_timestamp, True, program_date_time)
164 | return True
165 | elif self.part_timestamp is not None:
166 | part_diff = timestamp - self.part_timestamp
167 | if self.part_target * ts.HZ <= part_diff:
168 | self.part_timestamp = int(timestamp - max(0, part_diff - self.part_target * ts.HZ))
169 | self.m3u8.continuousPartial(self.part_timestamp)
170 | return False
171 |
172 | def pcr(self, pcr: int):
173 | pcr = (pcr - ts.HZ + ts.PCR_CYCLE) % ts.PCR_CYCLE
174 | diff = ((pcr - self.latest_pcr_value + ts.PCR_CYCLE) % ts.PCR_CYCLE) if self.latest_pcr_value is not None else 0
175 | self.latest_pcr_monotonic_timestamp_90khz += diff
176 | if self.latest_pcr_datetime is None: self.latest_pcr_datetime = datetime.now(timezone.utc) - timedelta(seconds=(1))
177 | self.latest_pcr_datetime += timedelta(seconds=(diff / ts.HZ))
178 | self.latest_pcr_value = pcr
179 |
180 | def scte35(self, scte35: SpliceInfoSection):
181 | if scte35.CRC32() != 0: return
182 |
183 | if scte35.splice_command_type == SpliceInfoSection.SPLICE_INSERT:
184 | splice_insert: SpliceInsert = cast(SpliceInsert, scte35.splice_command)
185 | id = str(splice_insert.splice_event_id)
186 | if splice_insert.splice_event_cancel_indicator: raise NotImplementedError()
187 | if not splice_insert.program_splice_flag: raise NotImplementedError()
188 | if splice_insert.out_of_network_indicator:
189 | attributes = { 'SCTE35-OUT': '0x' + ''.join([f'{b:02X}' for b in scte35[:]]) }
190 | if splice_insert.splice_immediate_flag or not splice_insert.splice_time.time_specified_flag:
191 | if self.latest_pcr_datetime is None: return
192 | start_date = self.latest_pcr_datetime
193 |
194 | if splice_insert.duration_flag:
195 | attributes['PLANNED-DURATION'] = str(splice_insert.break_duration.duration / ts.HZ)
196 | if splice_insert.break_duration.auto_return:
197 | self.scte35_in_queue.append((id, start_date + timedelta(seconds=(splice_insert.break_duration.duration / ts.HZ))))
198 | self.scte35_out_queue.append((id, start_date, None, attributes))
199 | else:
200 | if self.latest_pcr_value is None: return
201 | if self.latest_pcr_datetime is None: return
202 | start_date = timedelta(seconds=(((cast(int, splice_insert.splice_time.pts_time) + scte35.pts_adjustment - self.latest_pcr_value + ts.PCR_CYCLE) % ts.PCR_CYCLE) / ts.HZ)) + self.latest_pcr_datetime
203 |
204 | if splice_insert.duration_flag:
205 | attributes['PLANNED-DURATION'] = str(splice_insert.break_duration.duration / ts.HZ)
206 | if splice_insert.break_duration.auto_return:
207 | self.scte35_in_queue.append((id, start_date + timedelta(seconds=(splice_insert.break_duration.duration / ts.HZ))))
208 | self.scte35_out_queue.append((id, start_date, None, attributes))
209 | else:
210 | if splice_insert.splice_immediate_flag or not splice_insert.splice_time.time_specified_flag:
211 | if self.latest_pcr_datetime is None: return
212 | end_date = self.latest_pcr_datetime
213 | self.scte35_in_queue.append((id, end_date))
214 | else:
215 | if self.latest_pcr_value is None: return
216 | if self.latest_pcr_datetime is None: return
217 | end_date = timedelta(seconds=(((cast(int, splice_insert.splice_time.pts_time) + scte35.pts_adjustment - self.latest_pcr_value + ts.PCR_CYCLE) % ts.PCR_CYCLE) / ts.HZ)) + self.latest_pcr_datetime
218 | self.scte35_in_queue.append((id, end_date))
219 |
220 | elif scte35.splice_command_type == SpliceInfoSection.TIME_SIGNAL:
221 | time_signal: TimeSignal = cast(TimeSignal, scte35.splice_command)
222 | if self.latest_pcr_value is None: return
223 | if self.latest_pcr_datetime is None: return
224 | specified_time = self.latest_pcr_datetime
225 | if time_signal.splice_time.time_specified_flag:
226 | specified_time = timedelta(seconds=(((cast(int, time_signal.splice_time.pts_time) + scte35.pts_adjustment - self.latest_pcr_value + ts.PCR_CYCLE) % ts.PCR_CYCLE) / ts.HZ)) + self.latest_pcr_datetime
227 | for descriptor in scte35.descriptors:
228 | if descriptor.descriptor_tag != 0x02: return
229 | segmentation_descriptor: SegmentationDescriptor = cast(SegmentationDescriptor, descriptor)
230 | id = str(segmentation_descriptor.segmentation_event_id)
231 | if segmentation_descriptor.segmentation_event_cancel_indicator: raise NotImplementedError()
232 | if not segmentation_descriptor.program_segmentation_flag: raise NotImplementedError()
233 |
234 | if segmentation_descriptor.segmentation_event_id in SegmentationDescriptor.ADVERTISEMENT_BEGIN:
235 | attributes = { 'SCTE35-OUT': '0x' + ''.join([f'{b:02X}' for b in scte35[:]]) }
236 | if segmentation_descriptor.segmentation_duration_flag:
237 | attributes['PLANNED-DURATION'] = str(segmentation_descriptor.segmentation_duration / ts.HZ)
238 | self.scte35_out_queue.append((id, specified_time, None, attributes))
239 | elif segmentation_descriptor.segmentation_type_id in SegmentationDescriptor.ADVERTISEMENT_END:
240 | self.scte35_in_queue.append((id, specified_time))
241 |
--------------------------------------------------------------------------------
/biim/mp4/hevc.py:
--------------------------------------------------------------------------------
1 | from typing import cast
2 |
3 | from biim.mp4.box import trak, tkhd, mdia, mdhd, hdlr, minf, vmhd, dinf, stbl, stsd, hvc1
4 | from biim.util.bitstream import BitStream
5 |
6 | escapes = set([0x00, 0x01, 0x02, 0x03])
7 |
8 | def ebsp2rbsp(data: bytes | bytearray | memoryview) -> bytes:
9 | rbsp = bytearray(data[:2])
10 | length = len(data)
11 | for index in range(2, length):
12 | if index < length - 1 and data[index - 2] == 0x00 and data[index - 1] == 0x00 and data[index + 0] == 0x03 and data[index + 1] in escapes:
13 | continue
14 | rbsp.append(data[index])
15 | return bytes(rbsp)
16 |
17 | def hevcTrack(trackId: int, timescale: int, vps: bytes | bytearray | memoryview, sps: bytes | bytearray | memoryview, pps: bytes | bytearray | memoryview) -> bytes:
18 | general_profile_space: int | None = None
19 | general_tier_flag: bool | None = None
20 | general_profile_idc: int | None = None
21 | general_profile_compatibility_flags: bytes | None = None
22 | general_constraint_indicator_flags: bytes | None = None
23 | general_level_idc: int | None = None
24 | chroma_format_idc: int | None = None
25 | min_spatial_segmentation_idc: int | None = None
26 | bit_depth_luma_minus8: int | None = None
27 | bit_depth_chroma_minus8: int | None = None
28 | constant_frame_rate = 0
29 |
30 | sar_width = 1
31 | sar_height = 1
32 | pic_width_in_luma_samples: int | None = None
33 | pic_height_in_luma_samples: int | None = None
34 | codec_width: int | None = None
35 | codec_height: int | None = None
36 |
37 | def parseSPS():
38 | nonlocal general_profile_space
39 | nonlocal general_tier_flag
40 | nonlocal general_profile_idc
41 | nonlocal general_profile_compatibility_flags
42 | nonlocal general_constraint_indicator_flags
43 | nonlocal general_level_idc
44 | nonlocal chroma_format_idc
45 | nonlocal min_spatial_segmentation_idc
46 | nonlocal bit_depth_luma_minus8
47 | nonlocal bit_depth_chroma_minus8
48 |
49 | nonlocal pic_width_in_luma_samples
50 | nonlocal pic_height_in_luma_samples
51 | nonlocal codec_width
52 | nonlocal codec_height
53 |
54 | left_offset = 0
55 | right_offset = 0
56 | top_offset = 0
57 | bottom_offset = 0
58 |
59 | stream = BitStream(ebsp2rbsp(sps))
60 | stream.readByte(2) # remove header
61 |
62 | video_paramter_set_id = stream.readBits(4)
63 | max_sub_layers_minus1 = stream.readBits(3)
64 | temporal_id_nesting_flag = stream.readBool()
65 |
66 | general_profile_space = stream.readBits(2)
67 | general_tier_flag = stream.readBool()
68 | general_profile_idc = stream.readBits(5)
69 | general_profile_compatibility_flags = stream.readByte(4).to_bytes(4, byteorder='big')
70 | general_constraint_indicator_flags = stream.readByte(6).to_bytes(6, byteorder='big')
71 |
72 | general_level_idc = stream.readByte()
73 | sub_layer_profile_present_flag = []
74 | sub_layer_level_present_flag = []
75 | for _ in range(max_sub_layers_minus1):
76 | sub_layer_profile_present_flag.append(stream.readBool())
77 | sub_layer_level_present_flag.append(stream.readBool())
78 | if max_sub_layers_minus1 > 0:
79 | for _ in range(max_sub_layers_minus1, 8): stream.readBits(2)
80 | for i in range(max_sub_layers_minus1):
81 | if sub_layer_profile_present_flag[i]:
82 | stream.readByte() # sub_layer_profile_space, sub_layer_tier_flag, sub_layer_profile_idc
83 | stream.readByte(4) # sub_layer_profile_compatibility_flag
84 | stream.readByte(6)
85 | if sub_layer_level_present_flag[i]:
86 | stream.readByte()
87 |
88 | seq_parameter_set_id = stream.readUEG()
89 | chroma_format_idc = stream.readUEG()
90 | if chroma_format_idc == 3: stream.readBool()
91 | pic_width_in_luma_samples = stream.readUEG()
92 | pic_height_in_luma_samples = stream.readUEG()
93 | conformance_window_flag = stream.readBool()
94 | if conformance_window_flag:
95 | left_offset += stream.readUEG()
96 | right_offset += stream.readUEG()
97 | top_offset += stream.readUEG()
98 | bottom_offset += stream.readUEG()
99 | bit_depth_luma_minus8 = stream.readUEG()
100 | bit_depth_chroma_minus8 = stream.readUEG()
101 | log2_max_pic_order_cnt_lsb_minus4 = stream.readUEG()
102 | sub_layer_ordering_info_present_flag = stream.readBool()
103 | for _ in range(0 if sub_layer_ordering_info_present_flag else max_sub_layers_minus1, max_sub_layers_minus1 + 1):
104 | stream.readUEG() # max_dec_pic_buffering_minus1[i]
105 | stream.readUEG() # max_num_reorder_pics[i]
106 | stream.readUEG() # max_latency_increase_plus1[i]
107 | log2_min_luma_coding_block_size_minus3 = stream.readUEG()
108 | log2_diff_max_min_luma_coding_block_size = stream.readUEG()
109 | log2_min_transform_block_size_minus2 = stream.readUEG()
110 | log2_diff_max_min_transform_block_size = stream.readUEG()
111 | max_transform_hierarchy_depth_inter = stream.readUEG()
112 | max_transform_hierarchy_depth_intra = stream.readUEG()
113 | scaling_list_enabled_flag = stream.readBool()
114 | if scaling_list_enabled_flag:
115 | sps_scaling_list_data_present_flag = stream.readBool()
116 | if sps_scaling_list_data_present_flag:
117 | for sizeId in range(0, 4):
118 | for matrixId in range(0, 2 if sizeId == 3 else 6):
119 | scaling_list_pred_mode_flag = stream.readBool()
120 | if not scaling_list_pred_mode_flag:
121 | stream.readUEG() # scaling_list_pred_matrix_id_delta
122 | else:
123 | coefNum = min(64, (1 << (4 + (sizeId << 1))))
124 | if sizeId > 1: stream.readSEG()
125 | for _ in range(coefNum): stream.readSEG()
126 | amp_enabled_flag = stream.readBool()
127 | sample_adaptive_offset_enabled_flag = stream.readBool()
128 | pcm_enabled_flag = stream.readBool()
129 | if pcm_enabled_flag:
130 | stream.readByte()
131 | stream.readUEG()
132 | stream.readUEG()
133 | stream.readBool()
134 | num_short_term_ref_pic_sets = stream.readUEG()
135 | num_delta_pocs = 0
136 | for i in range(num_short_term_ref_pic_sets):
137 | inter_ref_pic_set_prediction_flag = False
138 | if i != 0: inter_ref_pic_set_prediction_flag = stream.readBool()
139 | if inter_ref_pic_set_prediction_flag:
140 | if i == num_short_term_ref_pic_sets: stream.readUEG()
141 | stream.readBool()
142 | stream.readUEG()
143 | next_num_delta_pocs = 0
144 | for _ in range(0, num_delta_pocs + 1):
145 | used_by_curr_pic_flag = stream.readBool()
146 | use_delta_flag = False
147 | if not used_by_curr_pic_flag: use_delta_flag = stream.readBool()
148 | if used_by_curr_pic_flag or use_delta_flag: next_num_delta_pocs += 1
149 | num_delta_pocs = next_num_delta_pocs
150 | else:
151 | num_negative_pics = stream.readUEG()
152 | num_positive_pics = stream.readUEG()
153 | num_delta_pocs = num_negative_pics + num_positive_pics
154 | for _ in range(num_negative_pics):
155 | stream.readUEG()
156 | stream.readBool()
157 | for _ in range(num_positive_pics):
158 | stream.readUEG()
159 | stream.readBool()
160 | long_term_ref_pics_present_flag = stream.readBool()
161 | if long_term_ref_pics_present_flag:
162 | num_long_term_ref_pics_sps = stream.readUEG()
163 | for i in range(num_long_term_ref_pics_sps):
164 | stream.readBits(log2_max_pic_order_cnt_lsb_minus4 + 4)
165 | stream.readBits(1)
166 |
167 | default_display_window_flag = False
168 | min_spatial_segmentation_idc = 0
169 | nonlocal sar_width
170 | nonlocal sar_height
171 | fps_fixed = False
172 | fps_den = 1
173 | fps_num = 1
174 |
175 | sps_temporal_mvp_enabled_flag = stream.readBool()
176 | strong_intra_smoothing_enabled_flag = stream.readBool()
177 | vui_parameters_present_flag = stream.readBool()
178 | if vui_parameters_present_flag:
179 | aspect_ratio_info_present_flag = stream.readBool()
180 | if aspect_ratio_info_present_flag:
181 | aspect_ratio_idc = stream.readByte()
182 | sar_w_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2]
183 | sar_h_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1]
184 | if 0 < aspect_ratio_idc and aspect_ratio_idc <= 16:
185 | sar_width = sar_w_table[aspect_ratio_idc - 1]
186 | sar_height = sar_h_table[aspect_ratio_idc - 1]
187 | elif aspect_ratio_idc == 255:
188 | sar_width = stream.readBits(16)
189 | sar_height = stream.readBits(16)
190 | overscan_info_present_flag = stream.readBool()
191 | if overscan_info_present_flag: stream.readBool()
192 | video_signal_type_present_flag = stream.readBool()
193 | if video_signal_type_present_flag:
194 | stream.readBits(3)
195 | stream.readBool()
196 | colour_description_present_flag = stream.readBool()
197 | if colour_description_present_flag:
198 | stream.readByte()
199 | stream.readByte()
200 | stream.readByte()
201 | chroma_loc_info_present_flag = stream.readBool()
202 | if chroma_loc_info_present_flag:
203 | stream.readUEG()
204 | stream.readUEG()
205 | neutral_chroma_indication_flag = stream.readBool()
206 | field_seq_flag = stream.readBool()
207 | frame_field_info_present_flag = stream.readBool()
208 | default_display_window_flag = stream.readBool()
209 | if default_display_window_flag:
210 | stream.readUEG()
211 | stream.readUEG()
212 | stream.readUEG()
213 | stream.readUEG()
214 | vui_timing_info_present_flag = stream.readBool()
215 | if vui_timing_info_present_flag:
216 | fps_den = stream.readByte(4)
217 | fps_num = stream.readByte(4)
218 | vui_poc_proportional_to_timing_flag = stream.readBool()
219 | if vui_poc_proportional_to_timing_flag:
220 | stream.readUEG()
221 | vui_hrd_parameters_present_flag = stream.readBool()
222 | if vui_hrd_parameters_present_flag:
223 | commonInfPresentFlag = 1
224 | nal_hrd_parameters_present_flag = False
225 | vcl_hrd_parameters_present_flag = False
226 | sub_pic_hrd_params_present_flag = False
227 | if commonInfPresentFlag:
228 | nal_hrd_parameters_present_flag = stream.readBool()
229 | vcl_hrd_parameters_present_flag = stream.readBool()
230 | if nal_hrd_parameters_present_flag or vcl_hrd_parameters_present_flag:
231 | sub_pic_hrd_params_present_flag = stream.readBool()
232 | if sub_pic_hrd_params_present_flag:
233 | stream.readByte()
234 | stream.readBits(5)
235 | stream.readBool()
236 | stream.readBits(5)
237 | bit_rate_scale = stream.readBits(4)
238 | cpb_size_scale = stream.readBits(4)
239 | if sub_pic_hrd_params_present_flag: stream.readBits(4)
240 | stream.readBits(5)
241 | stream.readBits(5)
242 | stream.readBits(5)
243 | for i in range(max_sub_layers_minus1 + 1):
244 | fixed_pic_rate_general_flag = stream.readBool()
245 | fps_fixed = fixed_pic_rate_general_flag
246 | fixed_pic_rate_within_cvs_flag = True
247 | cpbCnt = 1
248 | if not fixed_pic_rate_general_flag:
249 | fixed_pic_rate_within_cvs_flag = stream.readBool()
250 | low_delay_hrd_flag = False
251 | if fixed_pic_rate_within_cvs_flag: stream.readUEG()
252 | else:
253 | low_delay_hrd_flag = stream.readBool()
254 | if not low_delay_hrd_flag: cpbCnt = stream.readUEG() + 1
255 | if nal_hrd_parameters_present_flag:
256 | for j in range(0, cpbCnt):
257 | stream.readUEG()
258 | stream.readUEG()
259 | if sub_pic_hrd_params_present_flag:
260 | stream.readUEG()
261 | stream.readUEG()
262 | stream.readBool()
263 | if vcl_hrd_parameters_present_flag:
264 | for j in range(0, cpbCnt):
265 | stream.readUEG()
266 | stream.readUEG()
267 | if sub_pic_hrd_params_present_flag:
268 | stream.readUEG()
269 | stream.readUEG()
270 | stream.readBool()
271 | bitstream_restriction_flag = stream.readBool()
272 | if bitstream_restriction_flag:
273 | tiles_fixed_structure_flag = stream.readBool()
274 | motion_vectors_over_pic_boundaries_flag = stream.readBool()
275 | restricted_ref_pic_lists_flag = stream.readBool()
276 | min_spatial_segmentation_idc = stream.readUEG()
277 | max_bytes_per_pic_denom = stream.readUEG()
278 | max_bits_per_min_cu_denom = stream.readUEG()
279 | log2_max_mv_length_horizontal = stream.readUEG()
280 | log2_max_mv_length_vertical = stream.readUEG()
281 |
282 | sub_wc = 2 if chroma_format_idc in [1, 2] else 1
283 | sub_hc = 2 if chroma_format_idc == 1 else 1
284 | codec_width = pic_width_in_luma_samples - (left_offset + right_offset) * sub_wc
285 | codec_height = pic_height_in_luma_samples - (top_offset + bottom_offset) * sub_hc
286 | parseSPS()
287 |
288 | temporal_id_nesting_flag = False
289 | num_temporal_layers = 1
290 | def parseVPS():
291 | nonlocal num_temporal_layers
292 | nonlocal temporal_id_nesting_flag
293 | stream = BitStream(ebsp2rbsp(vps))
294 | stream.readByte(2) # remove header
295 |
296 | video_parameter_set_id = stream.readBits(4)
297 | stream.readBits(2)
298 | max_layers_minus1 = stream.readBits(6)
299 | max_sub_layers_minus1 = stream.readBits(3)
300 | num_temporal_layers = max_sub_layers_minus1 + 1
301 | temporal_id_nesting_flag = stream.readBool()
302 | parseVPS()
303 |
304 | parallelismType = 1
305 | def parsePPS():
306 | nonlocal parallelismType
307 | stream = BitStream(ebsp2rbsp(pps))
308 | stream.readByte(2)
309 |
310 | pic_parameter_set_id = stream.readUEG()
311 | seq_parameter_set_id = stream.readUEG()
312 | dependent_slice_segments_enabled_flag = stream.readBool()
313 | output_flag_present_flag = stream.readBool()
314 | num_extra_slice_header_bits = stream.readBits(3)
315 | sign_data_hiding_enabled_flag = stream.readBool()
316 | cabac_init_present_flag = stream.readBool()
317 | num_ref_idx_l0_default_active_minus1 = stream.readUEG()
318 | num_ref_idx_l1_default_active_minus1 = stream.readUEG()
319 | init_qp_minus26 = stream.readSEG()
320 | constrained_intra_pred_flag = stream.readBool()
321 | transform_skip_enabled_flag = stream.readBool()
322 | cu_qp_delta_enabled_flag = stream.readBool()
323 | if cu_qp_delta_enabled_flag:
324 | diff_cu_qp_delta_depth = stream.readUEG()
325 | cb_qp_offset = stream.readSEG()
326 | cr_qp_offset = stream.readSEG()
327 | pps_slice_chroma_qp_offsets_present_flag = stream.readBool()
328 | weighted_pred_flag = stream.readBool()
329 | weighted_bipred_flag = stream.readBool()
330 | transquant_bypass_enabled_flag = stream.readBool()
331 | tiles_enabled_flag = stream.readBool()
332 | entropy_coding_sync_enabled_flag = stream.readBool()
333 |
334 | parallelismType = 1 # slice-based parallel decoding
335 | if entropy_coding_sync_enabled_flag and tiles_enabled_flag:
336 | parallelismType = 0 # mixed-type parallel decoding
337 | elif entropy_coding_sync_enabled_flag:
338 | parallelismType = 3 # wavefront-based parallel decoding
339 | elif tiles_enabled_flag:
340 | parallelismType = 2 # tile-based parallel decoding
341 | parsePPS()
342 |
343 | hvcC = b''.join([
344 | bytes([
345 | 0x01,
346 | ((cast(int, general_profile_space) & 0x03) << 6) | ((1 if cast(bool, general_tier_flag) else 0) << 5) | ((cast(int, general_profile_idc) & 0x1F)),
347 | ]),
348 | cast(bytes | bytearray | memoryview, general_profile_compatibility_flags),
349 | cast(bytes | bytearray | memoryview, general_constraint_indicator_flags),
350 | bytes([
351 | cast(int, general_level_idc),
352 | (0xF0 | ((cast(int, min_spatial_segmentation_idc) & 0x0F00) >> 8)),
353 | ((cast(int, min_spatial_segmentation_idc) & 0x00FF) >> 0),
354 | (0xFC | (cast(int, parallelismType) & 0x03)),
355 | (0xFC | (cast(int, chroma_format_idc) & 0x03)),
356 | (0xF8 | (cast(int, bit_depth_luma_minus8) & 0x07)),
357 | (0xF8 | (cast(int, bit_depth_chroma_minus8) & 0x07)),
358 | 0x00,
359 | 0x00,
360 | ((cast(int, constant_frame_rate) & 0x03) << 6) | ((cast(int, num_temporal_layers) & 0x07) << 3) | (((1 if (cast(bool, temporal_id_nesting_flag)) else 0) << 2) | 3),
361 | 0x03,
362 | ]),
363 | bytes([
364 | 0x80 | 32,
365 | 0x00, 0x01,
366 | ((len(vps) & 0xFF00) >> 8),
367 | ((len(vps) & 0x00FF) >> 0),
368 | ]),
369 | vps,
370 | bytes([
371 | 0x80 | 33,
372 | 0x00, 0x01,
373 | ((len(sps) & 0xFF00) >> 8),
374 | ((len(sps) & 0x00FF) >> 0),
375 | ]),
376 | sps,
377 | bytes([
378 | 0x80 | 34,
379 | 0x00, 0x01,
380 | ((len(pps) & 0xFF00) >> 8),
381 | ((len(pps) & 0x00FF) >> 0),
382 | ]),
383 | pps
384 | ])
385 |
386 | presentation_width = (cast(int, codec_width) * sar_width + (sar_height - 1)) // sar_height
387 | presentation_height = cast(int, codec_height)
388 |
389 | return trak(
390 | tkhd(trackId, presentation_width, presentation_height),
391 | mdia(
392 | mdhd(timescale),
393 | hdlr('vide', 'videoHandler'),
394 | minf(
395 | vmhd(),
396 | dinf(),
397 | stbl(
398 | stsd(
399 | hvc1(hvcC, cast(int, codec_width), cast(int, codec_height))
400 | )
401 | )
402 | )
403 | )
404 | )
405 |
--------------------------------------------------------------------------------
/pseudo_quality.py:
--------------------------------------------------------------------------------
1 | # 以下は全て KonomiTV から検証のために移植
2 |
3 | import sys
4 | from pydantic import BaseModel, PositiveInt
5 | from typing import Literal
6 |
7 |
8 | # 品質を表す Pydantic モデル
9 | class Quality(BaseModel):
10 | is_hevc: bool # 映像コーデックが HEVC かどうか
11 | is_60fps: bool # フレームレートが 60fps かどうか
12 | width: PositiveInt # 縦解像度
13 | height: PositiveInt # 横解像度
14 | video_bitrate: str # 映像のビットレート
15 | video_bitrate_max: str # 映像の最大ビットレート
16 | audio_bitrate: str # 音声のビットレート
17 |
18 | # 品質の種類 (型定義)
19 | QUALITY_TYPES = Literal[
20 | '1080p-60fps',
21 | '1080p-60fps-hevc',
22 | '1080p',
23 | '1080p-hevc',
24 | '810p',
25 | '810p-hevc',
26 | '720p',
27 | '720p-hevc',
28 | '540p',
29 | '540p-hevc',
30 | '480p',
31 | '480p-hevc',
32 | '360p',
33 | '360p-hevc',
34 | '240p',
35 | '240p-hevc',
36 | ]
37 |
38 | # 映像と音声の品質
39 | QUALITY: dict[QUALITY_TYPES, Quality] = {
40 | '1080p-60fps': Quality(
41 | is_hevc = False,
42 | is_60fps = True,
43 | width = 1440,
44 | height = 1080,
45 | video_bitrate = '9500K',
46 | video_bitrate_max = '13000K',
47 | audio_bitrate = '256K',
48 | ),
49 | '1080p-60fps-hevc': Quality(
50 | is_hevc = True,
51 | is_60fps = True,
52 | width = 1440,
53 | height = 1080,
54 | video_bitrate = '3500K',
55 | video_bitrate_max = '5200K',
56 | audio_bitrate = '192K',
57 | ),
58 | '1080p': Quality(
59 | is_hevc = False,
60 | is_60fps = False,
61 | width = 1440,
62 | height = 1080,
63 | video_bitrate = '9500K',
64 | video_bitrate_max = '13000K',
65 | audio_bitrate = '256K',
66 | ),
67 | '1080p-hevc': Quality(
68 | is_hevc = True,
69 | is_60fps = False,
70 | width = 1440,
71 | height = 1080,
72 | video_bitrate = '3000K',
73 | video_bitrate_max = '4500K',
74 | audio_bitrate = '192K',
75 | ),
76 | '810p': Quality(
77 | is_hevc = False,
78 | is_60fps = False,
79 | width = 1440,
80 | height = 810,
81 | video_bitrate = '5500K',
82 | video_bitrate_max = '7600K',
83 | audio_bitrate = '192K',
84 | ),
85 | '810p-hevc': Quality(
86 | is_hevc = True,
87 | is_60fps = False,
88 | width = 1440,
89 | height = 810,
90 | video_bitrate = '2500K',
91 | video_bitrate_max = '3700K',
92 | audio_bitrate = '192K',
93 | ),
94 | '720p': Quality(
95 | is_hevc = False,
96 | is_60fps = False,
97 | width = 1280,
98 | height = 720,
99 | video_bitrate = '4500K',
100 | video_bitrate_max = '6200K',
101 | audio_bitrate = '192K',
102 | ),
103 | '720p-hevc': Quality(
104 | is_hevc = True,
105 | is_60fps = False,
106 | width = 1280,
107 | height = 720,
108 | video_bitrate = '2000K',
109 | video_bitrate_max = '3000K',
110 | audio_bitrate = '192K',
111 | ),
112 | '540p': Quality(
113 | is_hevc = False,
114 | is_60fps = False,
115 | width = 960,
116 | height = 540,
117 | video_bitrate = '3000K',
118 | video_bitrate_max = '4100K',
119 | audio_bitrate = '192K',
120 | ),
121 | '540p-hevc': Quality(
122 | is_hevc = True,
123 | is_60fps = False,
124 | width = 960,
125 | height = 540,
126 | video_bitrate = '1400K',
127 | video_bitrate_max = '2100K',
128 | audio_bitrate = '192K',
129 | ),
130 | '480p': Quality(
131 | is_hevc = False,
132 | is_60fps = False,
133 | width = 854,
134 | height = 480,
135 | video_bitrate = '2000K',
136 | video_bitrate_max = '2800K',
137 | audio_bitrate = '192K',
138 | ),
139 | '480p-hevc': Quality(
140 | is_hevc = True,
141 | is_60fps = False,
142 | width = 854,
143 | height = 480,
144 | video_bitrate = '1050K',
145 | video_bitrate_max = '1750K',
146 | audio_bitrate = '192K',
147 | ),
148 | '360p': Quality(
149 | is_hevc = False,
150 | is_60fps = False,
151 | width = 640,
152 | height = 360,
153 | video_bitrate = '1100K',
154 | video_bitrate_max = '1800K',
155 | audio_bitrate = '128K',
156 | ),
157 | '360p-hevc': Quality(
158 | is_hevc = True,
159 | is_60fps = False,
160 | width = 640,
161 | height = 360,
162 | video_bitrate = '750K',
163 | video_bitrate_max = '1250K',
164 | audio_bitrate = '128K',
165 | ),
166 | '240p': Quality(
167 | is_hevc = False,
168 | is_60fps = False,
169 | width = 426,
170 | height = 240,
171 | video_bitrate = '550K',
172 | video_bitrate_max = '650K',
173 | audio_bitrate = '128K',
174 | ),
175 | '240p-hevc': Quality(
176 | is_hevc = True,
177 | is_60fps = False,
178 | width = 426,
179 | height = 240,
180 | video_bitrate = '450K',
181 | video_bitrate_max = '650K',
182 | audio_bitrate = '128K',
183 | ),
184 | }
185 |
186 | def buildFFmpegOptions(
187 | quality: QUALITY_TYPES,
188 | output_ts_offset: int,
189 | ) -> list[str]:
190 | """
191 | FFmpeg に渡すオプションを組み立てる
192 |
193 | Args:
194 | quality (QUALITY_TYPES): 映像の品質
195 | output_ts_offset (int): 出力 TS のタイムスタンプオフセット (秒)
196 |
197 | Returns:
198 | list[str]: FFmpeg に渡すオプションが連なる配列
199 | """
200 |
201 | # オプションの入る配列
202 | options: list[str] = []
203 |
204 | # 入力
205 | ## -analyzeduration をつけることで、ストリームの分析時間を短縮できる
206 | ## -copyts で入力のタイムスタンプを出力にコピーする
207 | options.append('-f mpegts -analyzeduration 500000 -i pipe:0')
208 |
209 | # ストリームのマッピング
210 | ## 音声切り替えのため、主音声・副音声両方をエンコード後の TS に含む
211 | options.append('-map 0:v:0 -map 0:a:0 -map 0:a:1 -map 0:d? -ignore_unknown')
212 |
213 | # フラグ
214 | ## 主に FFmpeg の起動を高速化するための設定
215 | options.append('-fflags nobuffer -flags low_delay -max_delay 0 -tune zerolatency -max_interleave_delta 500K -threads auto')
216 |
217 | # 映像
218 | ## コーデック
219 | if QUALITY[quality].is_hevc is True:
220 | options.append('-vcodec libx265') # H.265/HEVC (通信節約モード)
221 | else:
222 | options.append('-vcodec libx264') # H.264
223 |
224 | ## バイトレートと品質
225 | options.append(f'-flags +cgop+global_header -vb {QUALITY[quality].video_bitrate} -maxrate {QUALITY[quality].video_bitrate_max}')
226 | options.append('-preset veryfast -aspect 16:9')
227 | if QUALITY[quality].is_hevc is True:
228 | options.append('-profile:v main')
229 | else:
230 | options.append('-profile:v high')
231 |
232 | ## 指定された品質の解像度が 1440×1080 (1080p) かつ入力ストリームがフル HD (1920×1080) の場合のみ、
233 | ## 特別に縦解像度を 1920 に変更してフル HD (1920×1080) でエンコードする
234 | video_width = QUALITY[quality].width
235 | video_height = QUALITY[quality].height
236 | # if (video_width == 1440 and video_height == 1080) and \
237 | # (self.recorded_video.video_resolution_width == 1920 and self.recorded_video.video_resolution_height == 1080):
238 | # video_width = 1920
239 |
240 | ## 最大 GOP 長 (秒)
241 | ## 30fps なら ×30 、 60fps なら ×60 された値が --gop-len で使われる
242 | # gop_length_second = self.GOP_LENGTH_SECOND
243 | gop_length_second = 2.5
244 |
245 | # インターレース映像のみ
246 | # if self.recorded_video.video_scan_type == 'Interlaced':
247 | if True:
248 | ## インターレース解除 (60i → 60p (フレームレート: 60fps))
249 | if QUALITY[quality].is_60fps is True:
250 | options.append(f'-vf yadif=mode=1:parity=-1:deint=1,scale={video_width}:{video_height}')
251 | options.append(f'-r 60000/1001 -g {int(gop_length_second * 60)}')
252 | ## インターレース解除 (60i → 30p (フレームレート: 30fps))
253 | else:
254 | options.append(f'-vf yadif=mode=0:parity=-1:deint=1,scale={video_width}:{video_height}')
255 | options.append(f'-r 30000/1001 -g {int(gop_length_second * 30)}')
256 | # プログレッシブ映像
257 | ## プログレッシブ映像の場合は 60fps 化する方法はないため、無視して元映像と同じフレームレートでエンコードする
258 | ## GOP は 30fps だと仮定して設定する
259 | # elif self.recorded_video.video_scan_type == 'Progressive':
260 | # options.append(f'-vf scale={video_width}:{video_height}')
261 | # options.append(f'-r 30000/1001 -g {int(gop_length_second * 30)}')
262 |
263 | # 音声
264 | ## 音声が 5.1ch かどうかに関わらず、ステレオにダウンミックスする
265 | options.append(f'-acodec aac -aac_coder twoloop -ac 2 -ab {QUALITY[quality].audio_bitrate} -ar 48000 -af volume=2.0')
266 |
267 | # 出力 TS のタイムスタンプオフセット
268 | options.append(f'-output_ts_offset {output_ts_offset}')
269 |
270 | # 出力
271 | options.append('-y -f mpegts') # MPEG-TS 出力ということを明示
272 | options.append('pipe:1') # 標準入力へ出力
273 |
274 | # オプションをスペースで区切って配列にする
275 | result: list[str] = []
276 | for option in options:
277 | result += option.split(' ')
278 |
279 | return result
280 |
281 | def buildHWEncCOptions(
282 | quality: QUALITY_TYPES,
283 | encoder_type: Literal['QSVEncC', 'NVEncC', 'VCEEncC', 'rkmppenc'],
284 | output_ts_offset: int,
285 | ) -> list[str]:
286 | """
287 | QSVEncC・NVEncC・VCEEncC・rkmppenc (便宜上 HWEncC と総称) に渡すオプションを組み立てる
288 |
289 | Args:
290 | quality (QUALITY_TYPES): 映像の品質
291 | encoder_type (Literal['QSVEncC', 'NVEncC', 'VCEEncC', 'rkmppenc']): エンコーダー (QSVEncC or NVEncC or VCEEncC or rkmppenc)
292 | output_ts_offset (int): 出力 TS のタイムスタンプオフセット (秒)
293 | Returns:
294 | list[str]: HWEncC に渡すオプションが連なる配列
295 | """
296 |
297 | # オプションの入る配列
298 | options: list[str] = []
299 |
300 | # 入力
301 | ## --input-probesize, --input-analyze をつけることで、ストリームの分析時間を短縮できる
302 | ## 両方つけるのが重要で、--input-analyze だけだとエンコーダーがフリーズすることがある
303 | options.append('--input-format mpegts --input-probesize 1000K --input-analyze 0.7 --input -')
304 | ## VCEEncC の HW デコーダーはエラー耐性が低く TS を扱う用途では不安定なので、SW デコーダーを利用する
305 | if encoder_type == 'VCEEncC':
306 | options.append('--avsw')
307 | ## QSVEncC・NVEncC・rkmppenc は HW デコーダーを利用する
308 | else:
309 | options.append('--avhw')
310 |
311 | # ストリームのマッピング
312 | ## 音声切り替えのため、主音声・副音声両方をエンコード後の TS に含む
313 | ## 音声が 5.1ch かどうかに関わらず、ステレオにダウンミックスする
314 | options.append('--audio-stream 1?:stereo --audio-stream 2?:stereo --data-copy timed_id3')
315 |
316 | # フラグ
317 | ## 主に HWEncC の起動を高速化するための設定
318 | options.append('-m avioflags:direct -m fflags:nobuffer+flush_packets -m flush_packets:1 -m max_delay:250000')
319 | options.append('-m max_interleave_delta:500K --lowlatency')
320 | ## QSVEncC と rkmppenc では OpenCL を使用しないので、無効化することで初期化フェーズを高速化する
321 | if encoder_type == 'QSVEncC' or encoder_type == 'rkmppenc':
322 | options.append('--disable-opencl')
323 | ## NVEncC では NVML によるモニタリングを無効化することで初期化フェーズを高速化する
324 | if encoder_type == 'NVEncC':
325 | options.append('--disable-nvml 1')
326 |
327 | # 映像
328 | ## コーデック
329 | if QUALITY[quality].is_hevc is True:
330 | options.append('--codec hevc') # H.265/HEVC (通信節約モード)
331 | else:
332 | options.append('--codec h264') # H.264
333 |
334 | ## ビットレート
335 | ## H.265/HEVC かつ QSVEncC の場合のみ、--qvbr (品質ベース可変ビットレート) モードでエンコードする
336 | ## それ以外は --vbr (可変ビットレート) モードでエンコードする
337 | if QUALITY[quality].is_hevc is True and encoder_type == 'QSVEncC':
338 | options.append(f'--qvbr {QUALITY[quality].video_bitrate} --fallback-rc')
339 | else:
340 | options.append(f'--vbr {QUALITY[quality].video_bitrate}')
341 | options.append(f'--max-bitrate {QUALITY[quality].video_bitrate_max}')
342 |
343 | ## H.265/HEVC の高圧縮化調整
344 | if QUALITY[quality].is_hevc is True:
345 | if encoder_type == 'QSVEncC':
346 | options.append('--qvbr-quality 30')
347 | elif encoder_type == 'NVEncC':
348 | options.append('--qp-min 23:26:30 --lookahead 16 --multipass 2pass-full --weightp --bref-mode middle --aq --aq-temporal')
349 |
350 | ## ヘッダ情報制御 (GOP ごとにヘッダを再送する)
351 | ## VCEEncC ではデフォルトで有効であり、当該オプションは存在しない
352 | if encoder_type != 'VCEEncC':
353 | options.append('--repeat-headers')
354 |
355 | ## 品質
356 | if encoder_type == 'QSVEncC':
357 | options.append('--quality balanced')
358 | elif encoder_type == 'NVEncC':
359 | options.append('--preset default')
360 | elif encoder_type == 'VCEEncC':
361 | options.append('--preset balanced')
362 | elif encoder_type == 'rkmppenc':
363 | options.append('--preset best')
364 | if QUALITY[quality].is_hevc is True:
365 | options.append('--profile main')
366 | else:
367 | options.append('--profile high')
368 | options.append('--dar 16:9')
369 |
370 | ## 最大 GOP 長 (秒)
371 | ## 30fps なら ×30 、 60fps なら ×60 された値が --gop-len で使われる
372 | # gop_length_second = self.GOP_LENGTH_SECOND
373 | gop_length_second = 2.5
374 |
375 | # GOP長を固定にする
376 | if encoder_type == 'QSVEncC':
377 | options.append('--strict-gop')
378 | elif encoder_type == 'NVEncC':
379 | options.append('--no-i-adapt')
380 |
381 | # インターレース映像
382 | # if self.recorded_video.video_scan_type == 'Interlaced':
383 | if True:
384 | # インターレース映像として読み込む
385 | options.append('--interlace tff')
386 | ## インターレース解除 (60i → 60p (フレームレート: 60fps))
387 | ## NVEncC の --vpp-deinterlace bob は品質が悪いので、代わりに --vpp-yadif を使う
388 | ## NVIDIA GPU は当然ながら Intel の内蔵 GPU よりも性能が高いので、GPU フィルタを使ってもパフォーマンスに問題はないと判断
389 | ## VCEEncC では --vpp-deinterlace 自体が使えないので、代わりに --vpp-yadif を使う
390 | if QUALITY[quality].is_60fps is True:
391 | if encoder_type == 'QSVEncC':
392 | options.append('--vpp-deinterlace bob')
393 | elif encoder_type == 'NVEncC' or encoder_type == 'VCEEncC':
394 | options.append('--vpp-yadif mode=bob')
395 | elif encoder_type == 'rkmppenc':
396 | options.append('--vpp-deinterlace bob_i5')
397 | options.append(f'--avsync vfr --gop-len {int(gop_length_second * 60)}')
398 | ## インターレース解除 (60i → 30p (フレームレート: 30fps))
399 | ## NVEncC の --vpp-deinterlace normal は GPU 機種次第では稀に解除漏れのジャギーが入るらしいので、代わりに --vpp-afs を使う
400 | ## NVIDIA GPU は当然ながら Intel の内蔵 GPU よりも性能が高いので、GPU フィルタを使ってもパフォーマンスに問題はないと判断
401 | ## VCEEncC では --vpp-deinterlace 自体が使えないので、代わりに --vpp-afs を使う
402 | else:
403 | if encoder_type == 'QSVEncC':
404 | options.append('--vpp-deinterlace normal')
405 | elif encoder_type == 'NVEncC' or encoder_type == 'VCEEncC':
406 | options.append('--vpp-afs preset=default')
407 | elif encoder_type == 'rkmppenc':
408 | options.append('--vpp-deinterlace normal_i5')
409 | options.append(f'--avsync vfr --gop-len {int(gop_length_second * 30)}')
410 | # プログレッシブ映像
411 | ## プログレッシブ映像の場合は 60fps 化する方法はないため、無視して元映像と同じフレームレートでエンコードする
412 | ## GOP は 30fps だと仮定して設定する
413 | # elif self.recorded_video.video_scan_type == 'Progressive':
414 | # options.append(f'--avsync vfr --gop-len {int(gop_length_second * 30)}')
415 |
416 | ## 指定された品質の解像度が 1440×1080 (1080p) かつ入力ストリームがフル HD (1920×1080) の場合のみ、
417 | ## 特別に縦解像度を 1920 に変更してフル HD (1920×1080) でエンコードする
418 | video_width = QUALITY[quality].width
419 | video_height = QUALITY[quality].height
420 | # if (video_width == 1440 and video_height == 1080) and \
421 | # (self.recorded_video.video_resolution_width == 1920 and self.recorded_video.video_resolution_height == 1080):
422 | # video_width = 1920
423 | options.append(f'--output-res {video_width}x{video_height}')
424 |
425 | # 音声
426 | options.append(f'--audio-codec aac:aac_coder=twoloop --audio-bitrate {QUALITY[quality].audio_bitrate}')
427 | options.append('--audio-samplerate 48000 --audio-filter volume=2.0 --audio-ignore-decode-error 30')
428 |
429 | # 出力 TS のタイムスタンプオフセット
430 | options.append(f'-m output_ts_offset:{output_ts_offset}')
431 |
432 | # 出力
433 | options.append('--output-format mpegts') # MPEG-TS 出力ということを明示
434 | options.append('--output -') # 標準入力へ出力
435 |
436 | # オプションをスペースで区切って配列にする
437 | result: list[str] = []
438 | for option in options:
439 | result += option.split(' ')
440 |
441 | return result
442 |
443 | def getEncoderCommand(encoder_type: Literal['FFmpeg', 'QSVEncC', 'NVEncC', 'VCEEncC', 'rkmppenc'], quality: QUALITY_TYPES, output_ts_offset: int) -> list[str]:
444 | # tsreadex のオプション
445 | ## 放送波の前処理を行い、エンコードを安定させるツール
446 | ## オプション内容は https://github.com/xtne6f/tsreadex を参照
447 | tsreadex_options = [
448 | 'tsreadex',
449 | # 取り除く TS パケットの10進数の PID
450 | ## EIT の PID を指定
451 | '-x', '18/38/39',
452 | # 特定サービスのみを選択して出力するフィルタを有効にする
453 | ## 有効にすると、特定のストリームのみ PID を固定して出力される
454 | ## 視聴対象の録画番組が放送されたチャンネルのサービス ID があれば指定する
455 | # '-n', f'{self.recorded_program.channel.service_id}' if self.recorded_program.channel is not None else '-1',
456 | '-n', '-1',
457 | # 主音声ストリームが常に存在する状態にする
458 | ## ストリームが存在しない場合、無音の AAC ストリームが出力される
459 | ## 音声がモノラルであればステレオにする
460 | ## デュアルモノを2つのモノラル音声に分離し、右チャンネルを副音声として扱う
461 | '-a', '13',
462 | # 副音声ストリームが常に存在する状態にする
463 | ## ストリームが存在しない場合、無音の AAC ストリームが出力される
464 | ## 音声がモノラルであればステレオにする
465 | '-b', '7',
466 | # 字幕ストリームが常に存在する状態にする
467 | ## ストリームが存在しない場合、PMT の項目が補われて出力される
468 | ## 実際の字幕データが現れない場合に5秒ごとに非表示の適当なデータを挿入する
469 | '-c', '5',
470 | # 文字スーパーストリームが常に存在する状態にする
471 | ## ストリームが存在しない場合、PMT の項目が補われて出力される
472 | '-u', '1',
473 | # 字幕と文字スーパーを aribb24.js が解釈できる ID3 timed-metadata に変換する
474 | ## +4: FFmpeg のバグを打ち消すため、変換後のストリームに規格外の5バイトのデータを追加する
475 | ## +8: FFmpeg のエラーを防ぐため、変換後のストリームの PTS が単調増加となるように調整する
476 | ## +4 は FFmpeg 6.1 以降不要になった (付与していると字幕が表示されなくなる) ため、
477 | ## FFmpeg 4.4 系に依存している Linux 版 HWEncC 利用時のみ付与する
478 | '-d', '13' if encoder_type != 'FFmpeg' and sys.platform == 'linux' else '9',
479 | # 標準入力からの入力を受け付ける
480 | '-',
481 | ]
482 |
483 | if encoder_type == 'FFmpeg':
484 | return tsreadex_options + ['|', 'ffmpeg'] + buildFFmpegOptions(quality, output_ts_offset)
485 | elif encoder_type == 'QSVEncC':
486 | return tsreadex_options + ['|', 'qsvencc'] + buildHWEncCOptions(quality, encoder_type, output_ts_offset)
487 | elif encoder_type == 'NVEncC':
488 | return tsreadex_options + ['|', 'nvencc'] + buildHWEncCOptions(quality, encoder_type, output_ts_offset)
489 | elif encoder_type == 'VCEEncC':
490 | return tsreadex_options + ['|', 'vceencc'] + buildHWEncCOptions(quality, encoder_type, output_ts_offset)
491 | elif encoder_type == 'rkmppenc':
492 | return tsreadex_options + ['|', 'rkmppenc'] + buildHWEncCOptions(quality, encoder_type, output_ts_offset)
493 | else:
494 | raise ValueError(f'Invalid encoder type: {encoder_type}')
495 |
--------------------------------------------------------------------------------