├── .editorconfig ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── biim ├── __init__.py ├── hls │ ├── m3u8.py │ └── segment.py ├── id3 │ ├── priv.py │ └── txxx.py ├── mp4 │ ├── avc.py │ ├── box.py │ ├── hevc.py │ └── mp4a.py ├── mpeg2ts │ ├── h264.py │ ├── h265.py │ ├── packetize.py │ ├── parser.py │ ├── pat.py │ ├── pes.py │ ├── pmt.py │ ├── scte.py │ ├── section.py │ └── ts.py ├── rtmp │ ├── amf0.py │ ├── demuxer.py │ ├── remuxer.py │ └── rtmp.py ├── util │ ├── bitstream.py │ ├── bytestream.py │ └── reader.py └── variant │ ├── codec.py │ ├── fmp4.py │ ├── handler.py │ └── mpegts.py ├── fmp4.py ├── main.py ├── multi.py ├── pseudo.html ├── pseudo.py ├── pyproject.toml ├── requirements.dev.txt ├── requirements.txt └── rtmp.py /.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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "basic" 3 | } 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.10.10' 2 | -------------------------------------------------------------------------------- /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/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/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/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 | ]) -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /pseudo.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 51 | 52 | -------------------------------------------------------------------------------- /pseudo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from typing import cast 4 | 5 | import asyncio 6 | from aiohttp import web 7 | 8 | import json 9 | import math 10 | import shlex 11 | 12 | from biim.mpeg2ts import ts 13 | from biim.mpeg2ts.packetize import packetize_section, packetize_pes 14 | from biim.mpeg2ts.parser import SectionParser, PESParser 15 | from biim.mpeg2ts.pat import PATSection 16 | from biim.mpeg2ts.pmt import PMTSection 17 | from biim.mpeg2ts.pes import PES 18 | 19 | import argparse 20 | from pathlib import Path 21 | 22 | async def keyframe_info(input: Path, targetduration: float = 3) -> list[tuple[int, float]]: 23 | options = ['-i', f'{input}', '-select_streams', 'v:0', '-show_packets', '-show_entries', 'packet=pts,dts,flags,pos', '-of', 'json'] 24 | prober = await asyncio.subprocess.create_subprocess_exec('ffprobe', *options, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL) 25 | 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']] 26 | 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 []) 27 | segments = [(pos, (end - begin) / ts.HZ) for (pos, begin), (_, end) in zip(filtered_frames[0:], filtered_frames[1:])] 28 | merged = [segments[0]] 29 | for segment in segments[1:]: 30 | if merged[-1][1] >= targetduration: 31 | merged.append((segment)) 32 | else: 33 | merged[-1] = (merged[-1][0], merged[-1][1] + segment[1]) 34 | return merged 35 | 36 | async def main(): 37 | loop = asyncio.get_running_loop() 38 | parser = argparse.ArgumentParser(description=('biim: HLS Pseudo VOD In-Memroy Origin')) 39 | 40 | parser.add_argument('-i', '--input', type=Path, required=True) 41 | parser.add_argument('-t', '--targetduration', type=int, nargs='?', default=3) 42 | parser.add_argument('-p', '--port', type=int, nargs='?', default=8080) 43 | 44 | args = parser.parse_args() 45 | input_path: Path = args.input 46 | input_file = open(args.input, 'rb') 47 | 48 | # setup pseudo playlist/segment 49 | segments = await keyframe_info(input_path, args.targetduration) 50 | num_of_segments = len(segments) 51 | target_duration = math.ceil(max(duration for _, duration in segments)) 52 | virtual_cache: str = 'identity' 53 | virtual_segments: list[asyncio.Future[bytes | bytearray | memoryview | None]] = [] 54 | processing: list[int] = [] 55 | process_queue: asyncio.Queue[int] = asyncio.Queue() 56 | 57 | async def m3u8(cache: str, s: int): 58 | virutal_playlist_header = '' 59 | virutal_playlist_header += f'#EXTM3U\n' 60 | virutal_playlist_header += f'#EXT-X-VERSION:6\n' 61 | virutal_playlist_header += f'#EXT-X-TARGETDURATION:{target_duration}\n' 62 | virutal_playlist_header += f'#EXT-X-PLAYLIST-TYPE:VOD\n' 63 | virtual_playlist_body = '' 64 | for seq, (_, duration) in enumerate(segments): 65 | if seq != 0 and seq == s: 66 | virtual_playlist_body += "#EXT-X-DISCONTINUITY\n" 67 | virtual_playlist_body += f"#EXTINF:{duration:.06f}\n" 68 | virtual_playlist_body += f"segment?seq={seq}&_={cache}\n" 69 | virtual_playlist_body += "\n" 70 | virtual_playlist_tail = '#EXT-X-ENDLIST\n' 71 | return virutal_playlist_header + virtual_playlist_body + virtual_playlist_tail 72 | 73 | async def playlist(request): 74 | nonlocal virtual_cache 75 | version = request.query['_'] if '_' in request.query else 'identity' 76 | t = float(request.query['t']) if 't' in request.query else 0 77 | seq = 0 78 | for segment in segments: 79 | if t < segment[1]: break 80 | t -= segment[1] 81 | seq += 1 82 | 83 | if not virtual_segments[seq].done() and not processing[seq]: 84 | virtual_cache = version 85 | 86 | result = await asyncio.shield(m3u8(virtual_cache, seq)) 87 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, text=result, content_type="application/x-mpegURL") 88 | async def segment(request): 89 | seq = request.query['seq'] if 'seq' in request.query else None 90 | 91 | if seq is None: 92 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type="video/mp2t") 93 | 94 | seq = int(seq) 95 | if seq < 0 or seq >= len(virtual_segments): 96 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=0'}, status=400, content_type="video/mp2t") 97 | 98 | if not virtual_segments[seq].done() and not processing[seq]: 99 | await process_queue.put(seq) 100 | await process_queue.join() 101 | 102 | body = await asyncio.shield(virtual_segments[seq]) 103 | 104 | if body is None: 105 | return await playlist(request) 106 | 107 | for prev in range(seq - 10, -1, -1): 108 | if not virtual_segments[prev].done(): break 109 | virtual_segments[prev] = asyncio.Future() 110 | 111 | return web.Response(headers={'Access-Control-Allow-Origin': '*', 'Cache-Control': 'max-age=3600'}, body=body, content_type="video/mp2t") 112 | 113 | # setup aiohttp 114 | app = web.Application() 115 | app.add_routes([ 116 | web.get('/playlist.m3u8', playlist), 117 | web.get('/segment', segment), 118 | ]) 119 | runner = web.AppRunner(app) 120 | await runner.setup() 121 | await loop.create_server(cast(web.Server, runner.server), '0.0.0.0', args.port) 122 | 123 | virtual_segments = [asyncio.Future[bytes | bytearray | memoryview | None]() for _ in range(num_of_segments)] 124 | processing = [False for _ in range(num_of_segments)] 125 | 126 | await process_queue.put(0) 127 | while True: 128 | seq = await process_queue.get() 129 | for idx in range(len(processing)): processing[idx] = False 130 | processing[seq] = True 131 | for future in virtual_segments: 132 | if not future.done(): future.set_result(None) 133 | virtual_segments = [asyncio.Future[bytes | bytearray | memoryview | None]() for _ in range(num_of_segments)] 134 | pos, _ = segments[seq] 135 | offset = sum((duration for _, duration in segments[:seq]), 0) 136 | process_queue.task_done() 137 | 138 | options = ['python3', '-c', f'\'import sys;\nfile=open("{shlex.quote(str(input_path))}","rb");\nfile.seek({pos});\nwhile file:\n sys.stdout.buffer.write(file.read(188 * 10))\''] + \ 139 | ['|', 'ffmpeg', '-f', 'mpegts', '-i', '-', '-map', '0:v:0', '-map', '0:a:0'] + \ 140 | ['-c:v', 'libx264', '-profile:v', 'baseline', '-tune', 'zerolatency', '-preset', 'ultrafast', "-pix_fmt", "yuv420p"] + \ 141 | ['-c:a', 'aac', '-ac', '2', '-ar', '48000'] + \ 142 | ['-output_ts_offset', f'{offset}', '-f', 'mpegts', '-', '-flags', '+cgop+global_header'] 143 | encoder = await asyncio.subprocess.create_subprocess_shell(" ".join(options), stdin=input_file, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL) 144 | reader = cast(asyncio.StreamReader, encoder.stdout) 145 | 146 | PAT_Parser: SectionParser[PATSection] = SectionParser(PATSection) 147 | PMT_Parser: SectionParser[PMTSection] = SectionParser(PMTSection) 148 | Video_Praser: PESParser[PES] = PESParser(PES) 149 | Audio_Praser: PESParser[PES] = PESParser(PES) 150 | LATEST_PAT: PATSection | None = None 151 | LATEST_PMT: PMTSection | None = None 152 | PAT_CC: int = 0 153 | PMT_PID: int | None = None 154 | PMT_CC: int = 0 155 | VIDEO_PID: int | None = None 156 | VIDEO_CC: int = 0 157 | AUDIO_PID: int | None = None 158 | AUDIO_CC: int = 0 159 | candidate = bytearray() 160 | 161 | while process_queue.empty(): 162 | if seq >= len(segments): break 163 | 164 | isEOF = False 165 | while True: 166 | try: 167 | sync_byte = await reader.readexactly(1) 168 | if sync_byte == ts.SYNC_BYTE: 169 | break 170 | elif sync_byte == b'': 171 | isEOF = True 172 | break 173 | except asyncio.IncompleteReadError: 174 | isEOF = True 175 | break 176 | if isEOF: 177 | break 178 | 179 | packet = None 180 | try: 181 | packet = ts.SYNC_BYTE + await reader.readexactly(ts.PACKET_SIZE - 1) 182 | except asyncio.IncompleteReadError: 183 | break 184 | 185 | PID = ts.pid(packet) 186 | if PID == 0x00: 187 | candidate += packet 188 | PAT_Parser.push(packet) 189 | for PAT in PAT_Parser: 190 | if PAT.CRC32() != 0: continue 191 | LATEST_PAT = PAT 192 | 193 | for program_number, program_map_PID in PAT: 194 | if program_number == 0: continue 195 | PMT_PID = program_map_PID 196 | 197 | for packet in packetize_section(PAT, False, False, 0, 0, PAT_CC): 198 | candidate += packet 199 | PAT_CC = (PAT_CC + 1) & 0x0F 200 | 201 | elif PID == PMT_PID: 202 | candidate += packet 203 | PMT_Parser.push(packet) 204 | for PMT in PMT_Parser: 205 | if PMT.CRC32() != 0: continue 206 | LATEST_PMT = PMT 207 | 208 | for stream_type, elementary_PID, _ in PMT: 209 | if stream_type == 0x1b: # H.264 210 | VIDEO_PID = elementary_PID 211 | elif stream_type == 0x24: # H.265 212 | VIDEO_PID = elementary_PID 213 | elif stream_type == 0x0F: # AAC 214 | AUDIO_PID = elementary_PID 215 | 216 | for packet in packetize_section(PMT, False, False, cast(int, PMT_PID), 0, PMT_CC): 217 | candidate += packet 218 | PMT_CC = (PMT_CC + 1) & 0x0F 219 | 220 | elif PID == VIDEO_PID: 221 | Video_Praser.push(packet) 222 | for VIDEO in Video_Praser: 223 | timestamp = cast(int, VIDEO.dts() or VIDEO.pts()) / ts.HZ 224 | 225 | if timestamp >= offset + segments[seq][1]: 226 | virtual_segments[seq].set_result(candidate) 227 | processing[seq] = False 228 | 229 | offset += segments[seq][1] 230 | seq += 1 231 | candidate = bytearray() 232 | if seq >= len(segments): 233 | break 234 | processing[seq] = True 235 | 236 | for packet in packetize_section(cast(PATSection, LATEST_PAT), False, False, 0, 0, PAT_CC): 237 | candidate += packet 238 | PAT_CC = (PAT_CC + 1) & 0x0F 239 | for packet in packetize_section(cast(PMTSection, LATEST_PMT), False, False, cast(int, PMT_PID), 0, PMT_CC): 240 | candidate += packet 241 | PMT_CC = (PMT_CC + 1) & 0x0F 242 | 243 | for packet in packetize_pes(VIDEO, False, False, cast(int, VIDEO_PID), 0, VIDEO_CC): 244 | candidate += packet 245 | VIDEO_CC = (VIDEO_CC + 1) & 0x0F 246 | 247 | elif PID == AUDIO_PID: 248 | Audio_Praser.push(packet) 249 | for AUDIO in Audio_Praser: 250 | for packet in packetize_pes(AUDIO, False, False, cast(int, AUDIO_PID), 0, AUDIO_CC): 251 | candidate += packet 252 | AUDIO_CC = (AUDIO_CC + 1) & 0x0F 253 | 254 | else: 255 | candidate += packet 256 | 257 | if __name__ == '__main__': 258 | asyncio.run(main()) 259 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /requirements.dev.txt: -------------------------------------------------------------------------------- 1 | hatch~=1.6.3 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp~=3.9.1 2 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------