From 4cfdead9b1e0d60e50115e2dd92387e535f64918 Mon Sep 17 00:00:00 2001 From: espes Date: Sat, 17 Jan 2015 23:43:18 +0200 Subject: [PATCH] It's working! --- README.md | 56 +++ aac.py | 287 +++++++++++++ airplay.py | 426 +++++++++---------- construct_utils.py | 218 ++++++++++ drm.py | 45 +- dyld_info.py | 2 +- loader.py | 13 +- mp4.py | 994 +++++++++++++++++++++++++++++++++++++++++++++ mpegts.py | 582 ++++++++++++++++++++++++++ 9 files changed, 2366 insertions(+), 257 deletions(-) create mode 100644 README.md create mode 100644 aac.py create mode 100644 construct_utils.py create mode 100644 mp4.py create mode 100644 mpegts.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c9a07b --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +Slave in the Magic Mirror +========================= + +What? +----- + +In short: Apple has a thing that lets you show what's on your iPhone or iPad or Mac on your Apple TV. This lets you see it on your Linux or Mac computer or media center too, maybe. + +This is an open-source implementation of [Apple AirPlay Mirroring](https://en.wikipedia.org/wiki/AirPlay#AirPlay_Mirroring) + +AirPlay Mirroring uses a funky mish-mash of standards wrapped in some DRM. The audio and video data is packed into a standard media container and handed to VLC. The DRM is handled by calling into the original AppleTV server binary using a pure-python ARM interpreter. + +It's not exactly production-ready, but try it out! + +How? +---- + +You need: + +- [VLC](https://www.videolan.org/vlc/) +- [PyPy](http://pypy.org/) with [pip](https://en.wikipedia.org/wiki/Pip_%28package_manager%29) +- A copy of the `airtunesd` binary from AppleTV firmware for AppleTV2,1 build 9A334v. Put it in this directory. + +Then: + +``` +pypy -m pip install construct biplist zeroconf cryptography + +pypy airplay.py +``` + +![screenshot](https://i.imgur.com/w5hEgsT.png) + + +Known Issues +------------ + +- Audio doesn't work + - The audio is packed into the MPEG-TS stream (mostly?) to-spec. The problem is no player correctly supports AAC-ELD and LATM... +- Rotating the device and launching some games crashes VLC + - lol +- I've only tested it with iOS 7.1.2 + + +Code Overview +------------- + +`airplay.py` - Main implementation of the AirPlay protocol + +`arm/` - Simple ARMv7 interpreter based on [arm-js](https://github.com/ozaki-r/arm-js) + +`drm.py` - Implementation of FairPlay SAP by calling into airtunesd + +`loader.py` `dyld_info.py` - Mach-O loader and minimal HLE for iOS binaries + +`aac.py` `mp4.py` `mpegts.py` - Implementations of bits of ISO/IEC 14496 Part 3, 10 and ISO/IEC 13818 Part 1. Enough to dump the AirPlay packets into a useful container. diff --git a/aac.py b/aac.py new file mode 100644 index 0000000..1d7f121 --- /dev/null +++ b/aac.py @@ -0,0 +1,287 @@ +# aac.py +# +# Copyright 2015, espes +# +# Licensed under GPL Version 2 or later +# + +from construct import * +from construct_utils import * + +# ISO/IEC 14496-3:2009 +# Coding of audio-visual objects - Part 3: Audio + +# 4.4.1 +def GASpecificConfig(channelConfiguration, + audioObjectType): + return Struct("GASpecificConfig", + Flag("frameLengthFlag"), + Flag("dependsOnCoreCoder"), + If(this.dependsOnCoreCoder, Bits("coreCoderDelay", 14)), + Flag("extensionFlag"), + If(lambda ctx: channelConfiguration(ctx) == 0, Bork("program_config_element")), + If(lambda ctx: audioObjectType(ctx) in (6, 20), Bork("layerNr")), + # P("K"), + # If(this.extensionFlag, Bork("extension")), + # If(lambda ctx: audioObjectType(ctx._) in (17, 19, 20, 23), Bork("aacStuff")), + Flag("extensionFlag3"), + If(this.extensionFlag3, Bork("extensionFlag3"))) + +# 4.6.20.3 +ELDEXT_TERM = 0 +ELDSpecificConfig = Struct("ELDSpecificConfig", + Flag("frameLengthFlag"), + Flag("aacSectionDataResilienceFlag"), + Flag("aacScalefactorDataResilienceFlag"), + Flag("aacSpectralDataResilienceFlag"), + + Flag("ldSbrPresentFlag"), + If(this.ldSbrPresentFlag, + Struct("ld", + Bits("ldSbrSamplingRate", 1), + Bits("ldSbrCrcFlag", 1), + Bork("ld_sbr_header"))), + + RepeatUntil(lambda obj, ctx: obj.eldExtType == ELDEXT_TERM, + Struct("eldext", + Bits("eldExtType", 4), + If(lambda ctx: ctx.eldExtType != ELDEXT_TERM, + Bork("eldExt")) + # ... + ))) + +# 1.6.2.1 +AudioObjectType = ExprAdapter( + Struct("audioObjectType", + Bits("audioObjectType", 5), + If(lambda ctx: ctx.audioObjectType == 31, + Bits("audioObjectTypeExt", 6))), + decoder = lambda obj, ctx: (obj.audioObjectType if obj.audioObjectType < 31 + else 32+obj.audioObjectTypeExt), + encoder = lambda obj, ctx: Container( + audioObjectType = 31 if obj >= 31 else obj, + audioObjectTypeExt=obj - 32 if obj >= 32 else None) +) + +# 1.6.3.4 + +frequencyIndex = [ + 97000, + 88200, + 64000, + 48000, + 44100, + 32000, + 24000, + 22050, + 16000, + 12000, + 11025, + 8000, + 7350 +] +SamplingFrequency = ExprAdapter( + Struct("samplingFrequency", + Bits("samplingFrequencyIndex", 4), + # P("sampling"), + If(lambda ctx: ctx.samplingFrequencyIndex == 0xf, + Bits("samplingFrequency", 24))), + decoder = lambda obj, ctx: obj.samplingFrequency or frequencyIndex[obj.samplingFrequencyIndex], + encoder = lambda obj, ctx: Container( + samplingFrequencyIndex=frequencyIndex.index(obj) if obj in frequencyIndex else 0xf, + samplingFrequency=obj) + ) + +AudioSpecificConfig = Struct("AudioSpecificConfig", + AudioObjectType, + SamplingFrequency, + Bits("channelConfiguration", 4), + If(lambda ctx: ctx.audioObjectType in (5, 29), + Struct("extensionConfig", + Rename("extensionSamplingFrequency", SamplingFrequency), + Rename("extensionAudioObjectType", AudioObjectType), + If(lambda ctx: ctx.extensionAudioObjectType == 22, + Bits("extensionChannelConfiguration", 4)))), + + # P("wut"), + + Switch("config", this.audioObjectType, { + 2: GASpecificConfig(this._.channelConfiguration, this._.audioObjectType), # AAC-LC + 39: ELDSpecificConfig, # AAC-ELD + # TODO + }, default=Bork("unimplemented audioObjectType")), + + If(lambda ctx: ctx.audioObjectType in (17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 39), + Struct("ep", + Bits("epConfig", 2), + If(lambda ctx: ctx.epConfig in (2, 3), Bork("epConfig")))), + # TODO + ) + +AudioSpecificConfig_bytes = BitStruct("AudioSpecificConfig", + Embed(AudioSpecificConfig), + ByteAlign()) + + +# 1.7.3 + +# This stuff is too messed up for construct. Really should be done manually... + +StreamMuxConfig = Struct("cfg", + Bit("audioMuxVersion"), + If(this.audioMuxVersion, + Flag("audioMuxVersionA")), + If(this.audioMuxVersionA, Bork("audioMuxVersionA")), + + If(this.audioMuxVersion, Bork("taraBufferFullnexx")), + Flag("allStreamsSameTimeFraming"), + Bits("numSubFrames", 6), + Bits("numProgram", 4), + + If(lambda ctx: ctx.numProgram > 0, Bork("numProgram")), + Bits("numLayer", 3), + If(lambda ctx: ctx.numLayer > 0, Bork("numLayer")), + Struct("layer", + # useSameConfig + # if not useSameConfig + If(lambda ctx: ctx._.audioMuxVersion == 0, + AudioSpecificConfig), + + Bits("frameLengthType", 3), + Embed(IfThenElse(None, + lambda ctx: ctx.frameLengthType == 0, + Struct(None, + Bits("latmBufferFullness", 8), + # TODO: coreFrameOffset is object type stuff + ), + Bork("frameLengthType") + )) + ), + + Flag("otherDataPresent"), + If(this.otherDataPresent, Bork("otherData")), + Flag("crcCheckPresent"), + If(this.crcCheckPresent, Bork("crcCheck")), + + # Probe("SM", show_stream=False, show_stack=False) + ) + +AudioMuxElement_1 = BitStruct("AudioMuxElement", + Flag("useSameStreamMux"), + If(lambda ctx: not ctx.useSameStreamMux, + StreamMuxConfig), + + # If(lambda ctx: ctx.cfg.numSubFrames != 0, Bork("numSubFrames")), + + # PayloadLengthInfo + # If(lambda ctx: not ctx.cfg.allStreamsSameTimeFraming, Bork()), + # If(lambda ctx: ctx.cfg.layer.frameLengthType != 0, Bork()), + ExprAdapter( + RepeatUntil(lambda obj, ctx: obj != 255, + Bits("MuxSlotLengthBytes", 8)), + decoder = lambda obj, ctx: sum(obj), + encoder = lambda obj, ctx: [255] * (obj // 255) + [obj % 255]), + # PayloadMux + Array(this.MuxSlotLengthBytes, Bits('payload', 8)), + + ByteAlign() + ) + +# 1.7.2 + +AudioSyncStream = StructWithLengthAdapter("AudioSyncStream", + StructLengthAdapter( + EmbeddedBitStruct( + Const(Bits("syncword", 11), 0x2b7), + Bits("audioMuxLengthBytes", 13)), + decoder = lambda obj, ctx: obj.audioMuxLengthBytes, + encoder = lambda length, obj, ctx: container_add(obj, audioMuxLengthBytes=length) + ), + Embed(AudioMuxElement_1), + Terminator + ) + + +def latm_mux(cfg, frames): + first_mux = DefaultingContainer( + useSameStreamMux = False, + cfg = cfg, + MuxSlotLengthBytes = len(frames[0]), + payload = map(ord, frames[0])) + + r = [AudioSyncStream.build(first_mux)] + + for f in frames[1:]: + mux = DefaultingContainer( + useSameStreamMux = True, + MuxSlotLengthBytes = len(f), + payload = map(ord, f)) + r.append(AudioSyncStream.build(mux)) + + return r + +def latm_mux_aac_lc(channels, sample_rate, frame_duration, frames): + assert frame_duration in (1024, 960) + assert len(frames) >= 1 + + cfg = Container( + audioMuxVersion = 0, + audioMuxVersionA = None, + allStreamsSameTimeFraming = True, + numSubFrames = 0, + numProgram = 0, + numLayer = 0, + layer = Container( + AudioSpecificConfig = Container( + audioObjectType = 2, # AAC-LC + samplingFrequency = sample_rate, + channelConfiguration = channels, + extensionConfig = None, + config = Container( + frameLengthFlag = frame_duration == 960, + dependsOnCoreCoder = False, + coreCoderDelay = None, + extensionFlag = True, + extensionFlag3 = False), + ep = Container(epConfig = 0)), + frameLengthType = 0, + latmBufferFullness = 0xff), #?? + otherDataPresent = False, + crcCheckPresent = False) + + return latm_mux(cfg, frames) + + +def latm_mux_aac_eld(channels, sample_rate, frame_duration, frames): + assert frame_duration in (512, 480) + assert len(frames) >= 1 + + cfg = Container( + audioMuxVersion = 0, + audioMuxVersionA = None, + allStreamsSameTimeFraming = True, + numSubFrames = 0, + numProgram = 0, + numLayer = 0, + layer = Container( + AudioSpecificConfig = Container( + audioObjectType = 39, # AAC-ELD + samplingFrequency = sample_rate, + channelConfiguration = channels, + extensionConfig = None, + config = Container( + frameLengthFlag = frame_duration == 480, + aacSectionDataResilienceFlag = False, + aacScalefactorDataResilienceFlag = False, + aacSpectralDataResilienceFlag = False, + ldSbrPresentFlag = False, + ld = None, + eldext = [Container(eldExtType=0)]), + ep = Container(epConfig = 0)), + frameLengthType = 0, + latmBufferFullness = 0xff), #?? + otherDataPresent = False, + crcCheckPresent = False) + + return latm_mux(cfg, frames) + diff --git a/airplay.py b/airplay.py index 535cd0a..e2702bd 100644 --- a/airplay.py +++ b/airplay.py @@ -1,21 +1,21 @@ # airplay.py # -# Copyright 2014, espes +# Copyright 2015, espes # -# Parts adapted from rtmp-livestreaming -# Copyright 2014, Michael Liao +# Parts adapted from Livestreamer # -# Licensed under GPL Version 3 or later +# Licensed under GPL Version 2 or later # +import os +import re import sys -import time -import select import socket import struct import threading +import subprocess import SocketServer -from collections import defaultdict, namedtuple, OrderedDict +from collections import defaultdict, namedtuple from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler import biplist @@ -26,64 +26,9 @@ import drm - -class BytesIO(object): - def __init__(self, data): - self._data = data - self._position = 0 - self._length = len(data) - - def read_uint8(self): - if self._position >= self._length: - raise IOError('EOF of BytesIO') - n = ord(self._data[self._position]) - self._position += 1 - return n - - def read_uint16(self): - return (self.read_uint8() << 8) + self.read_uint8() - - def read_uint24(self): - return ((self.read_uint8() << 16) - + (self.read_uint8() << 8) - + self.read_uint8()) - - def read_uint32(self): - return ((self.read_uint8() << 24) - + (self.read_uint8() << 16) - + (self.read_uint8() << 8) - + self.read_uint8()) - - def read_uint64(self): - return ((self.read_uint8() << 56) - + (self.read_uint8() << 48) - + (self.read_uint8() << 40) - + (self.read_uint8() << 32) - + (self.read_uint8() << 24) - + (self.read_uint8() << 16) - + (self.read_uint8() << 8) - + self.read_uint8()) - - def read_bytes(self, n): - if self._position + n > self._length: - raise IOError('Skip n bytes cause EOF.') - start = self._position - self._position = self._position + n - return self._data[start:self._position] - - def available(self): - return self._length - self._position - - def skip(self, n): - if self._position + n > self._length: - raise IOError('Skip n bytes cause EOF.') - self._position = self._position + n - - def left(self): - return self._data[self._position:] - - def __getitem__(self, key): - return self._data[key] +import aac +import mp4 +import mpegts class AirPlayMirroringVideoStream(object): @@ -102,102 +47,63 @@ def __init__(self, con, fd, key, iv): self.shutdown_request = False self.is_shutdown = threading.Event() + self.config_record = None + def shutdown(self): self.shutdown_request = True self.is_shutdown.wait() def handle(self): - self.of = open("/tmp/vids.h264", "wb") - while not self.shutdown_request: header = self.fd.read(128) if header == "": break - size, type_, unkn, timestamp = struct.unpack("> 32) + float(timestamp & 0xffffffff) / 2**32 data = self.fd.read(size) if type_ == 0: # video data decrypted_data = self.decryptor.update(data) + + self.con.viewer.handle_h264_nalus(timestamp, + list(self.parse_NALUs(decrypted_data))) - self._parse_NALUs(BytesIO(decrypted_data)) - elif type_ == 1: # codec data - self._parse_config_record(BytesIO(data)) + elif type_ == 1: # config record + self.config_record = mp4.AVCDecoderConfigurationRecord.parse(data) + + self.con.viewer.handle_h264_nalus(timestamp, + self.config_record.sequenceParameterSetNALUnit + + self.config_record.pictureParameterSetNALUnit) elif type_ == 2: # heartbeat pass else: print "wtf", size, type_, unkn, timestamp - self.of.close() - self.is_shutdown.set() - def _parse_config_record(self, s): - """Parses H.264 AVCC extradata and writes it out as Annex B""" - - ver = s.read_uint8() - if ver != 1: - raise Exception('Bad config version in AVCDecoderConfigurationRecord: %d' % ver) - - avc_profile_indication = s.read_uint8() - print 'profile:', avc_profile_indication - - profile_compatibility = s.read_uint8() - avc_level_indication = s.read_uint8() - length_size_minus_one = s.read_uint8() & 0x03 - print 'length_size_minus_one:', length_size_minus_one - - num_of_sps = s.read_uint8() & 0x1f - print 'num_of_sps:', num_of_sps - for i in range(num_of_sps): - sps_length = s.read_uint16() - spsNALU = s.read_bytes(sps_length) - print 'spsNALU Length:', sps_length - print 'spsNALU:', hex(ord(spsNALU[0])) - - self.of.write('\x00\x00\x00\x01') - self.of.write(spsNALU) - - num_of_pps = s.read_uint8() - print 'num_of_pps:', num_of_pps - for i in range(num_of_pps): - pps_length = s.read_uint16() - ppsNALU = s.read_bytes(pps_length) - print 'ppsNALU Length:', pps_length - print 'ppsNALU:', hex(ord(ppsNALU[0])) - - self.of.write('\x00\x00\x00\x01') - self.of.write(ppsNALU) - - self._nalu_length_size = length_size_minus_one + 1 - print 'data available shoud be 0:', s.available() - - def _parse_NALUs(self, s): - """Parses a series of H.264 AVCC NALUs and writes them out as Annex B""" - - nalu_length_size = self._nalu_length_size - - # split each NALUs and add '00000001' for each NALUs: - while s.available() > 0: - # the max value of nalu_length_size is 4 (=0x03 + 1) - length = 0 - if nalu_length_size==4: - length = s.read_uint32() - elif nalu_length_size==3: - length = s.read_uint24() - elif nalu_length_size==2: - length = s.read_uint16() - else: - length = s.read_uint8() - - if s.available() < length: + def parse_NALUs(self, s): + assert self.config_record is not None + length_size = self.config_record.lengthSizeMinusOne + 1 + assert 1 <= length_size <= 4 + + i = 0 + while i+length_size <= len(s): + length, = struct.unpack(">I", s[i:i+length_size].rjust(4, "\x00")) + i += length_size + nal = s[i:i+length]; + i += length + if len(nal) < length: raise Exception('bad NALU length: %d' % length) - self.of.write('\x00\x00\x00\x01') - self.of.write(s.read_bytes(length)) + yield nal + + assert i == len(s) class ThreadedHTTPServer(SocketServer.ThreadingMixIn, HTTPServer): @@ -237,8 +143,8 @@ def setup(self): BaseHTTPRequestHandler.setup(self) def do_GET(self): - print `self.path` - print self.headers + # print `self.path` + # print self.headers if self.path == "/stream.xml": response = """ @@ -268,16 +174,18 @@ def do_GET(self): self.send_error(404) def do_POST(self): - print `self.path` - print self.headers + # print `self.path` + # print self.headers if self.path == "/fp-setup": chal_data = self.rfile.read(int(self.headers["Content-Length"])) - print "chal", `chal_data` + # print "chal", `chal_data` + print "Calculating AirPlay challenge stage %d..." % self.sap_stage response = self.sap.challenge(3, chal_data, self.sap_stage) + print "Done!" self.sap_stage += 1 - print "resp", `response` + # print "resp", `response` self.send_response(200) self.send_header("Date", self.date_time_string()) @@ -295,13 +203,14 @@ def do_POST(self): bplist_data = self.rfile.read(int(self.headers["Content-Length"])) bplist = biplist.readPlistFromString(bplist_data) - print `bplist` + # print `bplist` assert bplist['deviceID'] == device_id iv = bplist['param2'] + print "Decrypting AirPlay key..." key = self.sap.decrypt_key(bplist['param1']) - print "decrypted key!", `key` + print "Done! AirPlay key:", key.encode("hex") con.handle_mirroring_video_stream(self.rfile, key, iv) self.close_connection = 1 @@ -320,62 +229,76 @@ def handle(self): hdrpieces = struct.unpack('!BBHII', packet[:12]) - v = hdrpieces[0] >> 6 + v = hdrpieces[0] >> 6 # Version assert v == 2 - # Padding - p = bool(hdrpieces[0] & 32) - # Extension header present - x = bool(hdrpieces[0] & 16) - # CSRC Count - cc = bool(hdrpieces[0] & 15) - # Marker bit - marker = bool(hdrpieces[1] & 128) - # Payload type - pt = hdrpieces[1] & 127 - # Sequence number - seq = hdrpieces[2] - # Timestamp - ts = hdrpieces[3] + p = bool(hdrpieces[0] & 32) # Padding + assert not p + x = bool(hdrpieces[0] & 16) # Extension header present + assert not x + cc = hdrpieces[0] & 15 # CSRC Count + assert cc == 0 + marker = bool(hdrpieces[1] & 128) # Marker bit + pt = hdrpieces[1] & 127 # Payload type + seq = hdrpieces[2] # Sequence number + ts = hdrpieces[3] # Timestamp ssrc = hdrpieces[4] - headerlen = 12 + cc * 4 - - # XXX throwing away csrc info for now - bytes = packet[headerlen:] - - if x: - # Only one extension header - xhdrtype, xhdrlen = struct.unpack('!HH', bytes[:4]) - xhdrdata = bytes[4:4+xhdrlen*4] - bytes = bytes[xhdrlen*4 + 4:] - else: - xhdrtype, xhdrdata = None, None - if p: - # padding - padlen = struct.unpack('!B', bytes[-1])[0] - if padlen: - bytes = bytes[:-padlen] + + bytes = packet[12:] decrypted_bytes = self.server.rtp.cipher.decryptor().update(bytes) bytes = decrypted_bytes + bytes[len(decrypted_bytes):] - # print "-", bytes.encode("hex") - self.server.rtp.handle_rtp_payload(ts, seq, bytes) class AirTunesRTPControlHandler(SocketServer.BaseRequestHandler): def handle(self): - print "RTP Control", self.request + # print "RTP Control", self.request + + packet, sock = self.request + hdrpieces = struct.unpack('!BBHIQI', packet) + + v = hdrpieces[0] >> 6 # Version + assert v == 2 + p = bool(hdrpieces[0] & 32) # Padding + assert not p + x = bool(hdrpieces[0] & 16) # Extension header present + # assert not x + cc = hdrpieces[0] & 15 # CSRC Count + assert cc == 0 + marker = bool(hdrpieces[1] & 128) # Marker bit + pt = hdrpieces[1] & 127 # Payload type + seq = hdrpieces[2] # Sequence number + ts = hdrpieces[3] # Timestamp + + # no SSRC + + ntp_ts = hdrpieces[4] + ts_next = hdrpieces[5] + + ntp_ts = (ntp_ts >> 32) + float(ntp_ts & 0xffffffff) / 2**32 + + # given as seconds since 1900, adjust it to seconds since 1970 + # maybe this'll make more sense when NTP is actualy implemented... + ntp_ts -= 2208988800 + + self.server.rtp.last_sync = (ts, ntp_ts) + + # print ts, ntp_ts, ts_next + + class AirTunesRTPTimingHandler(SocketServer.BaseRequestHandler): def handle(self): - print "RTP Timing", self.request + pass + # print "RTP Timing", self.request class AirTunesRTPEventHandler(SocketServer.StreamRequestHandler): def handle(self): - print "RTP Event" + pass + # print "RTP Event" class AirTunesRTP(object): # Reading: @@ -387,17 +310,21 @@ def __init__(self, con, url, sdp, key, iv): sdp_audio = sdp.media["audio"] assert sdp_audio.proto == "RTP/AVP" - assert sdp.fmtp[sdp_audio.fmt]["mode"] == "AAC-eld" - assert sdp.rtpmap[int(sdp_audio.fmt)].encoding == "mpeg4-generic" + audio_fmt = sdp.fmtp[sdp_audio.fmt] + assert audio_fmt["mode"] == "AAC-eld" + audio_map = sdp.rtpmap[int(sdp_audio.fmt)] + assert audio_map.encoding == "mpeg4-generic" + + self.sample_rate = int(audio_map.clock) + self.channels = int(audio_map.parameters[0]) + self.frame_duration = int(audio_fmt["constantDuration"]) self.cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) - self.of = open("/tmp/aud1.mp4", "wb") - - self.packet_queue = OrderedDict() - + self.next_seq = None + self.last_sync = None def start(self, is_udp, cport, tport): @@ -439,11 +366,21 @@ def shutdown(self): def handle_rtp_payload(self, ts, seq, payload): - if seq in self.packet_queue: + if self.next_seq is not None and seq < self.next_seq: return - self.packet_queue[seq] = (ts, payload) - # payload should be rfc3640 per spec, but looks like raw aac packets instead + if self.last_sync is not None: + sync_ts, sync_ntp_ts = self.last_sync + + samples_since_sync = ts - sync_ts + seconds_since_sync = samples_since_sync / float(self.sample_rate) + + timestamp = sync_ntp_ts + seconds_since_sync + + self.con.viewer.handle_aac_frame(timestamp, payload) + + self.next_seq = seq + 1 + @@ -466,7 +403,7 @@ def __init__(self, s): self.fmtp = {} for line in s.strip().split("\r\n"): - print `line` + # print `line` key, value = line.split("=", 1) if key == "a": @@ -532,17 +469,18 @@ def setup(self): def do_POST(self): - print `self.path` - print self.headers + # print `self.path` + # print self.headers if self.path == "/fp-setup": chal_data = self.rfile.read(int(self.headers["Content-Length"])) + # print "chal", `chal_data` - print "chal", `chal_data` - + print "Calculating AirTunes challenge stage %d..." % self.sap_stage response = self.sap.challenge(2, chal_data, self.sap_stage) + print "Done!" self.sap_stage += 1 - print "resp", `response` + # print "resp", `response` self.send_response(200) self.send_header("Content-Type", "application/octet-stream") @@ -557,20 +495,21 @@ def do_POST(self): self.send_error(404) def do_ANNOUNCE(self): - print `self.path` - print self.headers + # print `self.path` + # print self.headers device_id = int(self.headers["X-Apple-Device-ID"], 16) con = self.server.parent.connections[device_id] sdp_str = self.rfile.read(int(self.headers["Content-Length"])) - print sdp_str + # print sdp_str sdp = SDP(sdp_str) iv = sdp.attrs["aesiv"].decode("base64") + print "Decrypting AirTunes key..." key = self.sap.decrypt_key(sdp.attrs["fpaeskey"].decode("base64")) - print "decrypted key!", `key` + print "Done! AirTunes key:", key.encode("hex") con.rtp = AirTunesRTP(con, self.path, sdp, key, iv) @@ -590,14 +529,14 @@ def parse_transport(self, s): return r def do_SETUP(self): - print `self.path` - print self.headers + # print `self.path` + # print self.headers device_id = int(self.headers["X-Apple-Device-ID"], 16) con = self.server.parent.connections[device_id] transport = self.parse_transport(self.headers["Transport"]) - print transport + # print transport is_udp = "RTP/AVP/UDP" in transport con.rtp.start(is_udp, int(transport["control_port"]), int(transport["timing_port"])) @@ -617,7 +556,7 @@ def do_SETUP(self): "event_port=%i" % event_port, )) - print `rtransport` + # print `rtransport` # why don't I get /audio and /video ??? @@ -629,6 +568,9 @@ def do_SETUP(self): self.end_headers() def do_RECORD(self): + # print `self.path` + # print self.headers + self.send_response(200) self.send_header("Server", self.version_string()) self.send_header("CSeq", self.headers["CSeq"]) @@ -636,12 +578,12 @@ def do_RECORD(self): self.end_headers() def do_GET_PARAMETER(self): - print `self.path` - print self.headers + # print `self.path` + # print self.headers parameter = self.rfile.read(int(self.headers["Content-Length"])) - print `parameter` + # print `parameter` parameter = parameter.strip() @@ -656,11 +598,11 @@ def do_GET_PARAMETER(self): def do_SET_PARAMETER(self): - print `self.path` - print self.headers + # print `self.path` + # print self.headers setting = self.rfile.read(int(self.headers["Content-Length"])) - print `setting` + # print `setting` self.send_response(200) self.send_header("Server", self.version_string()) @@ -682,6 +624,55 @@ def do_FLUSH(self): self.end_headers() + + +class AirplayViewer(object): + def __init__(self, con): + self.con = con + + self.vlc = self.find_vlc() + assert self.vlc + + self.proc = subprocess.Popen([self.vlc, "--file-caching=3000", "-"], + stdin=subprocess.PIPE, + close_fds=True) + + self.muxer = mpegts.TSMuxer(self.proc.stdin, True, True) + self.muxer.write_tables() + + + def check_paths(self, exes, paths): + for path in paths: + for exe in exes: + path = os.path.expanduser(os.path.join(path, exe)) + if os.path.isfile(path): + return path + return None + + def find_vlc(self): + paths = os.environ.get("PATH", "").split(":") + if "darwin" in sys.platform: + paths += ["/Applications/VLC.app/Contents/MacOS/"] + paths += ["~/Applications/VLC.app/Contents/MacOS/"] + return self.check_paths(("VLC", "vlc"), paths) + else: + return self.check_paths(("vlc",), paths) + + def handle_aac_frame(self, ts, frame): + # hax + channels = self.con.rtp.channels + sample_rate = self.con.rtp.sample_rate + frame_duration = self.con.rtp.frame_duration + + packets = aac.latm_mux_aac_eld(channels, sample_rate, frame_duration, [frame]) + + self.muxer.mux_latm(ts, ''.join(packets)) + + def handle_h264_nalus(self, ts, nalus): + self.muxer.mux_h264(ts, ''.join('\x00\x00\x00\x01'+nalu for nalu in nalus)) + + + class AirplayConnection(object): def __init__(self, server): self.server = server @@ -690,6 +681,8 @@ def __init__(self, server): self.mirroring_thread = None self.rtp = None + + self.viewer = AirplayViewer(self) def handle_mirroring_video_stream(self, fd, key, iv): self.mirroring_thread = threading.current_thread() @@ -718,6 +711,7 @@ def __init__(self, airtunesd_filename=None): self.airplay_mirroring_port = 7100 self.zc = None + self.airplay_server = None self.airplay_mirroring_server = None self.airtunes_server = None @@ -726,7 +720,7 @@ def __init__(self, airtunesd_filename=None): # usually a mac adress, but ceebs self.device_id = ("11", "22", "33", "44", "55", "66") - self.service_name = "lolol" + self.service_name = "Slave in the Magic Mirror" self.connections = defaultdict(lambda: AirplayConnection(self)) @@ -754,7 +748,7 @@ def register_airplay(self, port): 'srcvers': u'120.2', } ) - self.zc.registerService(info) + self.zc.register_service(info) def register_airtunes(self, port): # See https://nto.github.io/AirPlay.html#servicediscovery-airtunesservice @@ -780,7 +774,7 @@ def register_airtunes(self, port): 'sf': u'0x4', } ) - self.zc.registerService(info) + self.zc.register_service(info) def run(self): @@ -802,14 +796,18 @@ def run(self): self.airplay_mirroring_server.parent = self self.airtunes_server.parent = self - print 'Starting servers' self.airplay_thread = threading.Thread( target=self.airplay_server.serve_forever) self.airplay_mirroring_thread = threading.Thread( target=self.airplay_mirroring_server.serve_forever) + # self.airtunes_thead = threading.Thread( + # target=self.airtunes_server.serve_forever) self.airplay_thread.start() self.airplay_mirroring_thread.start() + # self.airtunes_thead.start() + + print 'Ready' try: self.airtunes_server.serve_forever() @@ -819,14 +817,16 @@ def run(self): self.airplay_server.shutdown() self.airplay_mirroring_server.shutdown() + # self.airtunes_server.shutdown() self.airplay_thread.join() self.airplay_mirroring_thread.join() + # self.airtunes_thead.join() self.zc.close() if __name__ == "__main__": - server = AirplayServer("airtunesd_44") + server = AirplayServer("airtunesd") server.run() diff --git a/construct_utils.py b/construct_utils.py new file mode 100644 index 0000000..2042cbb --- /dev/null +++ b/construct_utils.py @@ -0,0 +1,218 @@ +# construct_utils.py +# +# Copyright 2015, espes +# +# Licensed under GPL Version 2 or later +# + +from construct import * + +# This file goes to fairly extreme lengths to work around inadequacies of construct. +# I've been planning on replacing construct because NIH, but this'll do for now... + +class Bork(Construct): + def __init__(self, msg=None): + Construct.__init__(self, None) + self.msg = msg + def _parse(self, stream, context): + raise Exception, self.msg + def _build(self, obj, stream, context): + raise Exception, self.msg + def _sizeof(self, context): + raise Exception, self.msg + + +class ParseOnly(Subconstruct): + def _build(self, obj, stream, context): + pass + +class BuildOnly(Subconstruct): + def _parse(self, stream, context): + if self.name in context: + return context[self.name] + return None + +class Hash(Subconstruct): + def __init__(self, subcon, startfunc, lengthfunc, hashfunc): + Subconstruct.__init__(self, subcon) + self.startfunc = startfunc + self.lengthfunc = lengthfunc + self.hashfunc = hashfunc + def _parse(self, stream, context): + newpos = self.startfunc(context) + origpos = stream.tell() + stream.seek(newpos, 0) + data = stream.read(self.lengthfunc(context)) + stream.seek(origpos, 0) + + readhash = self.subcon._parse(stream, context) + calchash = self.hashfunc(data, context) + assert readhash == calchash + return readhash + def _build(self, obj, stream, context): + newpos = self.startfunc(context) + origpos = stream.tell() + stream.seek(newpos, 0) + data = stream.read(self.lengthfunc(context)) + stream.seek(origpos, 0) + + calchash = self.hashfunc(data, context) + self.subcon._build(calchash, stream, context) + + +def UBInt24(name): + # return EmbeddedBitStruct(Bits(name, 24)) + return ExprAdapter(Array(3, UBInt8(name)), + decoder=lambda obj, ctx: (obj[0] << 16) | (obj[1] << 8) | obj[2], + encoder=lambda obj, ctx: + [(obj >> 16) & 0xff, (obj >> 8) & 0xff, obj & 0xff], + ) + + + +class DefaultingContainer(Container): + def __getattr__(self, name): + try: + return Container.__getattr__(self, name) + except AttributeError: + return None + +# Can't use Aligned because BitStreamReader can't seek -_- +class ByteAlign(Construct): + def __init__(self): + Construct.__init__(self, None) + def _parse(self, stream, context): + while stream.total_size % 8: + stream.read(1) + def _build(self, obj, stream, context): + while sum(map(len, stream.buffer)) % 8: + stream.write("\x00") + +class FixedLengthReader(object): + def __init__(self, substream, length): + self.substream = substream + self.length = length + self.startpos = self.tell() + self.endpos = self.startpos+length + def close(self): + self.substream.seek(self.endpos, 0) + def tell(self): + return self.substream.tell() + def seek(self, pos, whence=0): + r = self.substream.seek(pos, whence) + if not self.startpos<=self.tell()<=self.endpos: + raise ValueError("seek outside range?") + def read(self, count): + count = max(0, min(count, self.endpos-self.tell())) + return self.substream.read(count) + +class Restream2(Subconstruct): + def __init__(self, subcon, reader=None, writer=None): + Subconstruct.__init__(self, subcon) + self.reader = reader + self.writer = writer + def _parse(self, stream, context): + if self.reader: + stream2 = self.reader(stream, context) + obj = self.subcon._parse(stream2, context) + stream2.close() + else: + obj = self.subcon._parse(stream, context) + return obj + def _build(self, obj, stream, context): + if self.writer: + stream2 = self.writer(stream, context) + self.subcon._build(obj, stream2, context) + stream2.close() + else: + self.subcon._build(obj, stream, context) + def _sizeof(self, context): + raise NotImplementedError + + +def container_add(d, **kv): + c = d.copy() + c.update(kv) + return c + +class StructLengthAdapter(Adapter): + def __init__(self, subcon, encoder, decoder): + Adapter.__init__(self, subcon) + self.length_encoder = encoder + self.length_decoder = decoder + def _decode(self, obj, context): + context._struct_length = self.length_decoder(obj, context) + return obj + def _encode(self, obj, context): + return self.length_encoder(context._struct_length, obj, context) + + +def StructWithLengthAdapter(name, lengthcon, *datacon, **kw): + inclusive = kw.pop("inclusive", False) + return Struct(name, + Anchor("_startpos"), + BuildOnly(Value("_struct_length", lambda ctx: 0)), + lengthcon, + Anchor("_lengthpos"), + Restream2( + Embed(Struct(None, *datacon)), + reader = lambda stream, ctx: FixedLengthReader(stream, + ctx._struct_length-lengthcon._sizeof(ctx) if inclusive + else ctx._struct_length) + ), + Anchor("_endpos"), + + BuildOnly(Value("_struct_length", + lambda ctx: ctx._endpos-ctx._startpos if inclusive else ctx._endpos-ctx._lengthpos)), + BuildOnly(Pointer(this._startpos, lengthcon)), + allow_overwrite=True + ) + +def StructWithLength(name, lengthcon, *datacon, **kv): + return StructWithLengthAdapter(name, + StructLengthAdapter( + lengthcon, + encoder = lambda length, obj, con: length, + decoder = lambda obj, con: obj), + *datacon, + **kv) + + +# def P(name=None): +# return Probe(name, show_stream=False, show_stack=False) + + +if __name__ == "__main__": + ts = Struct("tmp", + UBInt8("a"), + UBInt8("b"), + StructWithLengthInStruct("tmpq", + StructLengthAdapter( + Embed(Struct("tmp2", + UBInt8("c"), + UBInt8("lollength"), + UBInt8("e"), + P("tmp2") + )), + encoder = lambda length, obj, ctx: container_add(obj, lollength=length), + decoder = lambda obj, ctx: obj.lollength + ), + Embed(Struct("tmp3", + UBInt8("d"), + P("tmp3") + )) + ) + ) + + tss = "abc\x01ed" + print ts.parse(tss) + + tc = Container( + a = 97, + b = 98, + tmpq = DefaultingContainer( + # lollength = 1, + c = 99, + e = 101, + d = 100)) + print `ts.build(tc)` diff --git a/drm.py b/drm.py index 0372c6d..5b03c56 100644 --- a/drm.py +++ b/drm.py @@ -60,19 +60,19 @@ def challenge(self, type_, data, stage): (type_, self.fpInfo, self.sapInfo, p_data, p_unkn, p_out_data, p_out_length, p_inout_stage)) - print "args", map(hex, (type_, self.fpInfo, self.sapInfo, - p_data, p_unkn, p_out_data, p_out_length, p_inout_stage)) + # print "args", map(hex, (type_, self.fpInfo, self.sapInfo, + # p_data, p_unkn, p_out_data, p_out_length, p_inout_stage)) - print "r", hex(r) + # print "r", hex(r) #assert r == 0 out_data = self.p.cpu.ld_word(p_out_data) - print "out_data", hex(out_data) + # print "out_data", hex(out_data) out_length = self.p.cpu.ld_word(p_out_length) - print "out_length", hex(out_length) + # print "out_length", hex(out_length) out_stage = self.p.cpu.ld_word(p_inout_stage) - print "out_stage", out_stage + # print "out_stage", out_stage if stage == 0: assert out_length == 0x8e @@ -96,39 +96,10 @@ def decrypt_key(self, param1): assert r == 0 out_data = self.p.cpu.ld_word(p_out_data) - print "out_data", hex(out_data) + # print "out_data", hex(out_data) out_length = self.p.cpu.ld_word(p_out_length) - print "out_length", hex(out_length) + # print "out_length", hex(out_length) assert out_length == 16 return self.p.copyout(out_data, out_length) - -if __name__ == "__main__": - - fp = FairPlaySAP("airtunesd_44") - - # print - # print "Stage 0" - # print - # r0 = fp.challenge(2, "46504C590301010000000004020003BB".decode("hex"), 0) - # print r0.encode("hex") - - # print - # print "Stage 1" - # print - # r1 = fp.challenge(2, "46504C590301030000000098018F1A9C7D0AF257B31F21F5C2D2BC814C032D457835AD0B06250574BBC7AB4A58CCA6EEAD2C911D7F3E1E7ED4C058955DFF3D5CEEF014387A985BDB34995015E3DFBDACC56047CB926E093B13E9FDB5E1EEE317C018BBC87FC5453C7671647DA686DA3D564875D03F8AEA9D60092DE06110BC7BE0C16F391C369C75344AE47F33ACFCF10E63A9B58BFCE215E96001C49E4BE967C5067F2A".decode("hex"), 1) - # print len(r1), r1.encode("hex") - - print - print "Stage 0" - print - r0 = fp.challenge(2, "46504c590201010000000004020001bb".decode("hex"), 0) - print r0.encode("hex") - assert r0 == "46504c59020102000000008202012157a656cf4046c01a75aad3fce23a29caf39d1a4f9b3c2db38519b0e9486e3da39f956d57db3eadc5f87c33f31ae1b5e28d23c45ac26dd15d8bdc04808440f4065657b1df9e12895c9b88a7a2b707ef2c23959012d4fcc733a0e8e3cc496e8bdebf2bfff7e9e906748dfe3ebbee8ebe798365ac0b4f9e40e034df3c7781849c".decode("hex") - - print - print "Stage 1" - print - r1 = fp.challenge(2, "46504c590201030000000098018f1a9c0955797af1df9d2ce22b3441090b61c98e9cd5f68cda65cc855bb0079bfceee13319f21873344cd4f4844556c2b78814cf78011c90f357a7f451dad8401e26edb8342f230c02ec0afdee5dfbf01629d0d2f8d2e01d0d5576dfe63a8cae86dc34beffb0fac0b44ba99e4c34017c9a9c01892e9ba4ffa2f032bd209ecd03b64e5da571516282ddc61b8514dac267430d82ecb64439".decode("hex"), 1) - print len(r1), r1.encode("hex") diff --git a/dyld_info.py b/dyld_info.py index ec1162e..35c7656 100644 --- a/dyld_info.py +++ b/dyld_info.py @@ -5,7 +5,7 @@ # Copyright 2010, KennyTM~ # Copyright 2014, espes # -# Licensed under GPL version 3 or later +# Licensed under GPL Version 3 or later # diff --git a/loader.py b/loader.py index c90107c..d064efa 100644 --- a/loader.py +++ b/loader.py @@ -6,6 +6,7 @@ # import sys +import time import inspect from macholib import MachO @@ -248,7 +249,7 @@ def exec_(self, arg=[], env=[]): self.run(exit_addr) - print "we're done!", self.cpu.regs[0] + # print "we're done!", self.cpu.regs[0] def call(self, func, args): if func in self.symbols: @@ -277,20 +278,20 @@ def call(self, func, args): self.cpu.regs[15] = addr self.cpu.cpsr.m = 0b10000 # user mode - print self.cpu.regs + # print self.cpu.regs self.run(exit_addr) - print "we're done!", self.cpu.regs[0] + # print "we're done!", self.cpu.regs[0] return self.cpu.regs[0] def run(self, exit_addr=None): self.running = True cnt = 0 - import time + tt = time.time() while self.running: cnt += 1 - if cnt % 100000 == 0: print cnt, cnt/(time.time()-tt) + # if cnt % 100000 == 0: print cnt, cnt/(time.time()-tt) self.cpu.branch_to = None pc = self.cpu.regs[15] @@ -331,7 +332,7 @@ def run(self, exit_addr=None): # print # print map(hex, self.cpu.regs) # raw_input() - print "did %d instructions" % cnt + # print "did %d instructions" % cnt diff --git a/mp4.py b/mp4.py new file mode 100644 index 0000000..986c588 --- /dev/null +++ b/mp4.py @@ -0,0 +1,994 @@ +# mp4.py +# +# Copyright 2015, espes +# +# Licensed under GPL Version 2 or later +# + +import struct +from construct import * + +from construct_utils import * +import aac + +# ISO/IEC 14496-1:2010 +# Coding of audio-visual objects - Part 1: Systems + +# 8.3.3 +class InstanceLengthAdapter(Adapter): + def _encode(self, obj, context): + if obj >= 128: + raise NotImplemented + return [0x80, 0x80, 0x80, obj] + def _decode(self, obj, context): + size = 0 + for v in obj: + size <<= 7 + size |= v & 0b1111111 + return size +InstanceLength = InstanceLengthAdapter( + RepeatUntil(lambda obj, ctx: (obj >> 7) == 0, + UBInt8("sizeOfInstance"))) + +# 7.2.6.14.3.4 +def ByteArray(name): + return Struct(name, + InstanceLength, + String("data", this.sizeOfInstance)) + +def ExpandableTag(name, tags, *subcons): + tagcons = UBInt8("tag") + if isinstance(tags, int): + tagcons = Const(tagcons, tags) + elif tags is not None: + tagcons = OneOf(tagcons, tags) + + if subcons: + datacons = Embed(TunnelAdapter( + Field("data", this.sizeOfInstance), + Struct(None, *subcons))) + else: + datacons = String("data", this.sizeOfInstance) + + return Struct(name, + tagcons, + InstanceLength, + datacons) + +# 7.2.2.2 +def BaseDescriptor(name, tags, *subcons): + return ExpandableTag(name, tags, *subcons) + +# 7.2.6.3 +MP4_OD_Tag = 0x11 +ObjectDescrTag = 0x01 +ObjectDescriptor_data = Struct(None, + EmbeddedBitStruct( + Bits("ObjectDescriptorID", 10), + Flag("URL_Flag"), + Padding(5)), # reserved + Embed(IfThenElse(None, this.URL_Flag, + PascalString("URLstring", UBInt8("URLlength")), + Struct(None, + Range(1, 255, LazyBound("esDescr", lambda: ES_Descriptor)), + Range(0, 255, LazyBound("ociDescr", lambda: OCI_Descriptor)), + Range(0, 255, LazyBound("ipmpDescrPtr", lambda: IPMP_DescriptorPointer)), + Range(0, 255, LazyBound("ipmpDescr", lambda: IPMP_Descriptor))))), + Range(0, 255, LazyBound("extDescr", lambda: ExtensionDescriptor))) + +ObjectDescriptor = BaseDescriptor("OD", ObjectDescrTag, + Embed(ObjectDescriptor_data)) + +# 7.2.6.4 +MP4_IOD_Tag = 0x10 +InitialObjectDescrTag = 0x2 +InitialObjectDescriptor_data = Struct(None, + EmbeddedBitStruct( + Bits("ObjectDescriptorID", 10), + Flag("URL_Flag"), + Flag("includeInlineProfileLevelFlag"), + Padding(4)), # reserved + Embed(IfThenElse(None, this.URL_Flag, + PascalString("URLstring", UBInt8("URLlength")), + Struct(None, + UBInt8("ODProfileLevelIndication"), + UBInt8("sceneProfileLevelIndication"), + UBInt8("audioProfileLevelIndication"), + UBInt8("visualProfileLevelIndication"), + UBInt8("graphicsProfileLevelIndication"), + Range(0, 255, LazyBound("esDescr", lambda: ES_Descriptor)), # should be [1...255]?? + Range(0, 255, LazyBound("ociDescr", lambda: OCI_Descriptor)), + Range(0, 255, LazyBound("ipmpDescrPtr", lambda: IPMP_DescriptorPointer)), + Range(0, 255, LazyBound("ipmpDescr", lambda: IPMP_Descriptor)), + Range(0, 255, LazyBound("toolListDescr", lambda: IPMP_ToolListDescriptor))))), + Range(0, 255, LazyBound("extDescr", lambda: ExtensionDescriptor))) + +InitialObjectDescriptor = BaseDescriptor("IOD", InitialObjectDescrTag, + Embed(InitialObjectDescriptor_data)) + +# 7.2.6 +ObjectDescriptorBase = BaseDescriptor("OD", (ObjectDescrTag, InitialObjectDescrTag), + Embed(Switch("OD", this.tag, + { ObjectDescrTag: ObjectDescriptor_data, + InitialObjectDescrTag: InitialObjectDescriptor_data }, default=Bork()))) + +MP4_ObjectDescriptorBase = BaseDescriptor("OD", (MP4_OD_Tag, MP4_IOD_Tag), + Embed(Switch("OD", this.tag, + { MP4_OD_Tag: ObjectDescriptor_data, + MP4_IOD_Tag: InitialObjectDescriptor_data }, default=Bork()))) + +# 7.2.6.9 +ContentIdentDescrTag = 0x07 +SupplContentIdentDescrTag = 0x08 +IP_IdentificationDataSet = BaseDescriptor("ipIDS", + range(ContentIdentDescrTag, SupplContentIdentDescrTag+1)) + +# 7.2.6.12 +IPI_DescrPointerTag = 0x09 +IPI_DescrPointer = BaseDescriptor("ipiPtr", IPI_DescrPointerTag, + UBInt16("IPI_ES_Id")) + +# 7.2.6.13 +IPMP_DescrPtrTag = 0x0a +IPMP_DescriptorPointer = BaseDescriptor("ipmpDesrPtr", IPMP_DescrPtrTag) + +# 7.2.6.14 +IPMP_DescrTag = 0x0b +IPMP_Descriptor = BaseDescriptor("ipmpDescr", IPMP_DescrTag) + +# 7.2.6.14.3.2 +IPMP_ToolTag = 0x61 +IPMP_Tool = BaseDescriptor("ipmpTool", IPMP_ToolTag) + +# 7.2.6.14.3 +IPMP_ToolsListDescrTag = 0x60 +IPMP_ToolListDescriptor = BaseDescriptor("toolListDescr", IPMP_ToolsListDescrTag, + Range(0, 255, IPMP_Tool)) + +# 7.2.6.15 +QoS_DescrTag = 0x0c +QoS_Descriptor = BaseDescriptor("qosDescr", QoS_DescrTag) + +# 7.2.6.16 +ExtDescrTagStartRange = 0x6a +ExtDescrTagEndRange = 0xfe +ExtensionDescriptor = BaseDescriptor("extDescr", + range(ExtDescrTagStartRange, ExtDescrTagEndRange+1)) + +# 7.2.6.17 +RegistrationDescrTag = 0x0d +RegistrationDescriptor = BaseDescriptor("regDescr", RegistrationDescrTag) + +# 7.2.6.18 +OCIDescrTagStartRange = 0x40 +OCIDescrTagEndRange = 0x5f +OCI_Descriptor = BaseDescriptor("ociDescr", + range(OCIDescrTagStartRange, OCIDescrTagEndRange+1)) + +# 7.2.6.18.6 +LanguageDescrTag = 0x43 +LanguageDescriptor = BaseDescriptor("langDescr", LanguageDescrTag, + Array(3, UBInt8("languageCode"))) + +# 7.2.6.20 +DecSpecificInfoTag = 0x05 +def DecoderSpecificInfo(objectTypeFunc=None): + if objectTypeFunc is None: + return BaseDescriptor("decSpecificInfo", DecSpecificInfoTag) + + return BaseDescriptor("decSpecificInfo", DecSpecificInfoTag, + Switch("data", lambda ctx: objectTypeFunc(ctx._), { + 0x40: aac.AudioSpecificConfig_bytes, + }, default=String("data", this.sizeOfInstance))) + +# 7.2.6.20 +ProfileLevelIndicationIndexDescrTag = 0x14 +ProfileLevelIndicationIndexDescriptor = BaseDescriptor( + "profileLevelIndicationIndexDescriptor", ProfileLevelIndicationIndexDescrTag, + UBInt8("profileLevelIndicationIndex")) + +# 7.2.6.6 +DecoderConfigDescrTag = 0x04 +DecoderConfigDescriptor = BaseDescriptor("decoderConfigDescr", DecoderConfigDescrTag, + EmbeddedBitStruct( + Bits("objectTypeIndication", 8), + Bits("streamType", 6), + Bits("upStream", 1), + Padding(1), #reserved + Bits("bufferSizeDB", 24)), + UBInt32("maxBitrate"), + UBInt32("avgBitrate"), + Optional(DecoderSpecificInfo(this.objectTypeIndication)), + Range(0, 255, ProfileLevelIndicationIndexDescriptor)) + +# 7.3.2.3 +SLConfigDescrTag = 0x06 +SLConfigDescriptor = BaseDescriptor("slConfigDescr", SLConfigDescrTag, + UBInt8("predefined"), + If(lambda ctx: ctx.predefined == 0, + Embed(Struct(None, + EmbeddedBitStruct( + Flag("useAccessUnitStartFlag"), + Flag("useAccessUnitEndFlag"), + Flag("useRandomAccessPointFlag"), + Flag("hasRandomAccessUnitsOnlyFlag"), + Flag("usePaddingFlag"), + Flag("useTimeStampsFlag"), + Flag("useIdleFlag"), + Flag("durationFlag"), + Bits("timeStampResolution", 32), + Bits("OCRResolution", 32), + Bits("timeStampLength", 8), + Bits("OCRLength", 8), + Bits("AU_Length", 8), + Bits("instantBitrateLength", 8), + Bits("degredationPriorityLength", 4), + Bits("AU_seqNumLength", 5), + Bits("packetSeqNumLength", 5), + Padding(2)), #reserved + If(this.durationFlag, + Embed(Struct(None, + UBInt32("timeScale"), + UBInt16("accessUnitDuration"), + UBInt16("compositionUnitDuration")))), + If(lambda ctx: not ctx.useTimeStampsFlag, + Bits("startDecodingTimeStamp", this.timeStampLength), + Bits("startCompositionTimeStamp", this.timeStampLength)))))) + + +# 7.2.6.5 +ES_DescrTag = 0x03 +ES_Descriptor = BaseDescriptor("ES", ES_DescrTag, + UBInt16("ES_ID"), + EmbeddedBitStruct( + Flag("streamDependenceFlag"), + Flag("URL_Flag"), + Flag("OCRstreamFlag"), + Bits("streamPriority", 5)), + If(this.streamDependenceFlag, + UBInt16("dependsOn_ES_ID")), + If(this.URL_Flag, + PascalString("URLstring", UBInt8("URLlength"))), + If(this.OCRstreamFlag, + UBInt16("OCR_ES_Id")), + DecoderConfigDescriptor, + SLConfigDescriptor, + Optional(IPI_DescrPointer), + Range(0, 255, IP_IdentificationDataSet), + Range(0, 255, IPMP_DescriptorPointer), + Range(0, 255, LanguageDescriptor), + Optional(QoS_Descriptor), + Optional(RegistrationDescriptor), + Range(0, 255, ExtensionDescriptor)) + + + +# ISO/IEC 14496-12:2012 +# Coding of audio-visual objects - Part 12: ISO base media file format + +box_data = {} + +def make_box(fourcc=None): + typeCons = String("type", 4) + if fourcc: + typeCons = Const(typeCons, fourcc) + + return StructWithLength(fourcc or "box", + UBInt32("size"), + + Anchor("_box_start"), + typeCons, + # If(lambda ctx: ctx.size == 1, + # UBInt64("largesize")), + If(lambda ctx: ctx.type == 'uuid', + String("usertype", 16)), + Anchor("_header_end"), + ParseOnly(Value("data_size", + lambda ctx: ctx.size - 4 - (ctx._header_end - ctx._box_start))), + Probe("box", show_stream=False, show_stack=False), + Switch("data", this.type, box_data, default=Bork("unimplemented box")), + OptionalGreedyRange(LazyBound("children", lambda: Box)), + Terminator, + inclusive=True) + +Box = make_box() + + +def define_box(fourcc, datacons=None): + if datacons: + box_data[fourcc] = Rename(fourcc, datacons) + else: + box_data[fourcc] = Struct(fourcc) + return make_box(fourcc) + + +def BaseBox(*subcons): + return Struct(None, *subcons) + +# 4.2 +def FullBox(*subcons): + return Struct(None, + UBInt8("version"), + UBInt24("flags"), + *subcons) + +# 4.3 +FileTypeBox = define_box("ftyp", + BaseBox( + String("major_brand", 4), + UBInt32("major_brand_version"), + OptionalGreedyRange(String("compatible_brands", 4)))) + +# 8.1 +MediaDataBox = define_box("mdat", + OnDemand(Field("data", this.data_size))) + +# 8.1.2 +FreeSpaceBox = define_box("free", + Padding(this.data_size)) + +# 8.2.1 +MovieBox = define_box("moov") + +# 8.2.2 +MovieHeaderBox = define_box("mvhd", + FullBox( + Embed(IfThenElse(None, lambda ctx: ctx.version == 1, + Struct(None, + UBInt64("creation_time"), + UBInt64("modification_time"), + UBInt32("timescale"), + UBInt64("duration")), + Struct(None, + UBInt32("creation_time"), + UBInt32("modification_time"), + UBInt32("timescale"), + UBInt32("duration")))), + UBInt32("rate"), + UBInt16("volume"), + Padding(2), #reserved + Padding(8), #reserved + Array(9, UBInt32("matrix")), + Padding(6*4), #pre_defined + UBInt32("next_track_id"))) + +# 8.3.1 +TrackBox = define_box("trak") + +# 8.3.2 +TrackHeaderBox = define_box("tkhd", + FullBox( + Embed(IfThenElse(None, lambda ctx: ctx.version == 1, + Struct(None, + UBInt64("creation_time"), + UBInt64("modification_time"), + UBInt32("track_id"), + Padding(4), #reserved + UBInt64("duration")), + Struct(None, + UBInt32("creation_time"), + UBInt32("modification_time"), + UBInt32("track_id"), + Padding(4), #reserved + UBInt32("duration")))), + Padding(8), #reserved + UBInt16("layer"), + UBInt16("alternate_group"), + UBInt16("volume"), + Padding(2), #reserved + Array(9, UBInt32("matrix")), + UBInt32("width"), + UBInt32("height"))) + +# 8.4.1 +MediaBox = define_box("mdia") + +# 8.4.2 +MediaHeaderBox = define_box("mdhd", + FullBox( + Embed(IfThenElse(None, lambda ctx: ctx.version == 1, + Struct(None, + UBInt64("creation_time"), + UBInt64("modification_time"), + UBInt32("timescale"), + UBInt64("duration")), + Struct(None, + UBInt32("creation_time"), + UBInt32("modification_time"), + UBInt32("timescale"), + UBInt32("duration")))), + EmbeddedBitStruct( + Padding(1), + Array(3, Bits("language", 5))), + Padding(2))) #pre_defined + +# 8.4.3 +HandlerBox = define_box("hdlr", + FullBox( + Padding(4), #pre_defined + String("handler_type", 4), + Padding(3*4), #reserved + CString("name"))) + +# 8.4.4 +MediaInformationBox = define_box("minf"), + +VideoMediaHeaderBox = define_box("vmhd", + FullBox( + UBInt16("graphicsmode"), + Array(3, UBInt16("opcolor")))) + +# 8.4.5.3 +SoundMediaHeaderBox = define_box("smhd", + FullBox( + UBInt16("balance"), + Padding(2))) #reserved + + +# 8.5.1 +SampleTableBox = define_box("stbl") + +# 8.5.2 +SampleDescriptionBox = define_box("stsd", + FullBox( + PrefixedArray( + Rename("entry", Box), # TODO + UBInt32("entry_count")))) + +# 8.5.2.2 +def SampleEntry(*subcons): + return Struct(None, + Padding(6), #reserved + UBInt16("data_reference_index"), + *subcons) + +PixelAspectRatioBox = define_box("pasp", + BaseBox( + UBInt32("hSpacing"), + UBInt32("vSpacing"))) + +CleanApertureBox = define_box("clap", + BaseBox( + UBInt32("cleanApertureWidthN"), + UBInt32("cleanApertureWidthD"), + UBInt32("cleanApertureHeightN"), + UBInt32("cleanApertureHeightD"), + UBInt32("horizOffN"), + UBInt32("horizOffD"), + UBInt32("vertOffN"), + UBInt32("vertOffD"))) + +def VisualSampleEntry(*subcons): + return SampleEntry( + Padding(2), #pre_defined + Padding(2), #reserved + Padding(4*3), #pre_defined + UBInt16("width"), + UBInt16("height"), + UBInt32("horizresolution"), + UBInt32("vertresolution"), + Padding(4), #reserved + UBInt16("frame_count"), + String("compressorname", 32, padchar="\x00"), + UBInt16("depth"), + Padding(2), #pre_defined + *(subcons + ( + Optional(CleanApertureBox), + Optional(PixelAspectRatioBox) + ))) + +def AudioSampleEntry(*subcons): + return SampleEntry( + Padding(8), #reserved + UBInt16("channelcount"), + UBInt16("samplesize"), + Padding(2), #pre_defined + Padding(2), #reserved + UBInt32("samplerate"), + *subcons) + +# 8.6.1.2 +TimeToSampleBox = define_box("stts", + FullBox( + PrefixedArray( + Struct("sample_entry", + UBInt32("sample_count"), + UBInt32("sample_delta")), + UBInt32("entry_count")))) + +# 8.6.2 +SyncSampleBox = define_box("stss", + FullBox( + PrefixedArray( + UBInt32("sample_number"), + UBInt32("entry_count")))) + +# 8.6.5 +EditBox = define_box("edts") + +# 8.6.6 +EditListBox = define_box("elst", + FullBox( + UBInt32("entry_count"), + Array(this.entry_count, + Struct('entry', + Embed(IfThenElse(None, lambda ctx: ctx._.version == 1, + Struct(None, + UBInt64("segment_duration"), + UBInt64("media_time")), + Struct(None, + UBInt32("segment_duration"), + UBInt32("media_time")))), + UBInt16("media_rate_integer"), + UBInt16("media_rate_fraction"))))) + +# 8.7.1 +DataInformationBox = define_box("dinf") + +# 8.7.2 +DataEntryUrlBox = define_box("url ", + FullBox( + Optional(CString("location")))) +DataEntryUrnBox = define_box("urn ", + FullBox( + CString("name"), + Optional(CString("location")))) +DataReferenceBox = define_box("dref", + FullBox( + PrefixedArray( + Rename("data_entry", Box), #TODO + UBInt32("entry_count")))) + +# 8.7.3.2 +SampleSizeBox = define_box("stsz", + FullBox( + UBInt32("sample_size"), + UBInt32("sample_count"), + If(lambda ctx: ctx.sample_size == 0, + OnDemand(Array(this.sample_count, + UBInt32("sample_size_list")))))) + +# 8.7.4 +SampleToChunkBox = define_box("stsc", + FullBox( + PrefixedArray( + Struct("sample_entry", + UBInt32("first_chunk"), + UBInt32("samples_per_chunk"), + UBInt32("sample_description_index")), + UBInt32("entry_count")))) + +# 8.7.5 +ChunkOffsetBox = define_box("stco", + FullBox( + PrefixedArray( + UBInt32("chunk_offset"), + UBInt32("entry_count")))) + +# 8.10.1 +UserDataBox = define_box("udta") + +# 8.11.1 +MetaBox = define_box("meta", + FullBox( + LazyBound("theHandler", lambda: HandlerBox), + # TODO + OptionalGreedyRange(Rename("other_boxes", Box)))) + + +# ISO/IEC 14496-14:2003 +# Coding of audio-visual objects - Part 14: MP4 file format + +# 5.1 +ObjectDescriptorBox = define_box("iods", + FullBox(MP4_ObjectDescriptorBase)) + +# 5.6 +ESDBox = define_box("esds", FullBox(ES_Descriptor)) +MP4AudioSampleEntry = define_box("mp4a", + AudioSampleEntry(Rename("ES", ESDBox))) + + +# ISO/IEC 14496-15:2014 +# Coding of audio-visual objects +# Part 15: Carriage of NAL unit structured video in ISO base media file format + +# 5.3.3.1 +AVCDecoderConfigurationRecord = Struct("avcC", + UBInt8("configurationVersion"), + UBInt8("AVCProfileIndication"), + UBInt8("profile_compatibility"), + UBInt8("AVCLevelIndication"), + EmbeddedBitStruct( + Padding(6), #reserved + Bits("lengthSizeMinusOne", 2), + Padding(3), #reserved + Bits("numOfSequenceParameterSets", 5)), + Array(this.numOfSequenceParameterSets, + PascalString("sequenceParameterSetNALUnit", + UBInt16("sequenceParameterSetLength"))), + PrefixedArray( + PascalString("pictureParameterSetNALUnit", + UBInt16("pictureParameterSetLength")), + UBInt8("numOfPictureParameterSets")), + If(lambda ctx: ctx.AVCProfileIndication in (100, 110, 122, 144), + Optional(Struct("ext", + EmbeddedBitStruct( + Padding(6), #reserved + Bits("chroma_format", 2), + Padding(5), #reserved + Bits("bit_depth_luma_minus8", 3), + Padding(5), #reserved + Bits("bit_depth_chromw_minus8", 3)), + PrefixedArray( + PascalString("sequenceParameterSetExt", + UBInt16("sequenceParameterSetExtLength")), + UBInt8("numOfSequenceParameterSetExt")))))) + +# 5.4.2.1 +AVCConfigurationBox = define_box("avcC", + BaseBox(AVCDecoderConfigurationRecord)) +MPEG4BitRateBox = define_box("btrt", + BaseBox( + UBInt32("bufferSizeDB"), + UBInt32("maxBitrate"), + UBInt32("avgBitrate"))) +MPEG4ExtensionDescriptorsBox = define_box("m4ds", + BaseBox( + Range(0, 255, BaseDescriptor("descr", None)))) +AVCSampleEntry = define_box("avc1", + VisualSampleEntry( + AVCConfigurationBox, + Optional(MPEG4BitRateBox), + Optional(MPEG4ExtensionDescriptorsBox))) + + +# derp +IlstBox = define_box("ilst", BaseBox(Padding(this._.data_size))) + +MP4 = GreedyRange(Box) + + +def mux_aac_eld(channels, sample_rate, sample_size, frame_duration, frames): + + # ftyp 4.3 File Type + # moov 8.2.1 Movie + # mvhd 8.2.2 Movie Header + # trak 8.3.1 Track + # tkhd 8.3.2 Track Header + # mdia 8.4.1 Media + # mdhd 8.4.2 Media Header + # hdlr 8.4.3 Handler Reference + # minf 8.4.4 Media Information + # smhd 8.4.5.3 Sound Media Header + # dinf 8.7.1 Data Information + # dref Data Reference ... + # stbl 8.5.1 Sample Table + # stsd 8.5.2 Sample Description + # mp4a + # esds + # stts 8.6.1.2 Decoding Time to Sample + # stsc 8.7.4 Sample To Chunk + # stsz? 8.7.3.2 Sample Size + # stco 8.7.5 Chunk Offset - Index of each chunk into the file + + assert frame_duration in (512, 480) + + # sample_rate in hz + # sample_size in bits + # frame_duration in samples + duration = 1000 * frame_duration * len(frames) / sample_rate # in ms + + ftyp = DefaultingContainer( + type = 'ftyp', + data = Container( + major_brand = 'M4A ', + major_brand_version = 512, + compatible_brands = ['isom', 'iso2']), + children = []) + + mvhd = DefaultingContainer( + type = 'mvhd', + data = Container( + version = 0, + flags = 0, + creation_time = 0, + modification_time = 0, + timescale = 1000, + duration = duration, + rate = 6, # wtf + volume = 0x100, + matrix = [ + 0x10000, 0, 0, + 0, 0x10000, 0, + 0, 0, 0x40000000 + ], + next_track_id = 2), + children = []) + + + audioSpecificConfig = Container( + audioObjectType = 39, # AAC-ELD + samplingFrequency = sample_rate, + channelConfiguration = channels, + extensionConfig = None, + config = Container( + frameLengthFlag = frame_duration == 480, + aacSectionDataResilienceFlag = False, + aacScalefactorDataResilienceFlag = False, + aacSpectralDataResilienceFlag = False, + ldSbrPresentFlag = False, + ld = None, + eldext = [Container(eldExtType=0)]), + ep = Container(epConfig = 0), + remaining = [0, 0]) + + esds = DefaultingContainer( + type = 'esds', + data = Container( + version = 0, + flags = 0, + ES = Container( + tag = 3, # ES_DescrTag + sizeOfInstance = 36, + ES_ID = 1, + streamDependenceFlag = False, + URL_Flag = False, + OCRstreamFlag = False, + streamPriority = 0, + dependsOn_ES_ID = None, + URLstring = None, + OCR_ES_Id = None, + decoderConfigDescr = Container( + tag = 4, # DecoderConfigDescrTag + sizeOfInstance = 22, + objectTypeIndication = 0x40, # Audio ISO/IEC 14496-3 + streamType = 5, + upStream = 0, + bufferSizeDB = 0, + maxBitrate = 0, + avgBitrate = 0, + decSpecificInfo = Container( + tag = 5, # DecSpecificInfoTag + sizeOfInstance = 4, + data = audioSpecificConfig), + profileLevelIndicationIndexDescriptor = []), + slConfigDescr = Container( + tag = 6, # SLConfigDescrTag + sizeOfInstance = 1, + predefined = 2), + ipiPtr = None, + ipIDS = [], + ipmpDesrPtr = [], + langDescr = [], + qosDescr = None, + regDescr = None, + extDescr = [])), + children = []) + + mp4a = DefaultingContainer( + type = 'mp4a', + data = Container( + data_reference_index = 1, + channelcount = channels, + samplesize = sample_size, + samplerate = sample_rate << 16, + ES = esds), + children = []) + + stsd = DefaultingContainer( + type = 'stsd', + data = Container( + version = 0, + flags = 0, + entry = [ mp4a ]), + children = []) + + stts = DefaultingContainer( + type = 'stts', + data = Container( + version = 0, + flags = 0, + sample_entry = [ + Container( + sample_count = len(frames), + sample_delta = frame_duration) + ]), + children = []) + + stsc = DefaultingContainer( + type = 'stsc', + data = Container( + version = 0, + flags = 0, + sample_entry = [ + Container( + first_chunk = 1, + samples_per_chunk = len(frames), + sample_description_index = 1) + ]), + children = []) + + stsz = DefaultingContainer( + type = 'stsz', + data = Container( + version = 0, + flags = 0, + sample_size = 0, + sample_count = len(frames), + sample_size_list = map(len, frames)), + children = []) + + stco = DefaultingContainer( + type = 'stco', + data = Container( + version = 0, + flags = 0, + chunk_offset = [ + 0xff998877, # hax + ]), + children = []) + + minf = DefaultingContainer( + type = 'minf', + children = [ + DefaultingContainer( + type = 'smhd', + data = Container( + version = 0, + flags = 0, + balance = 0), + children = []), + + DefaultingContainer( + type = 'dinf', + children = [ + DefaultingContainer( + type = 'dref', + data = Container( + version = 0, + flags = 0, + data_entry = [ + DefaultingContainer( + type = 'url ', + data = Container( + version = 0, + flags = 1, + location = None), + children = []) + ]), + children = []) + ]), + + DefaultingContainer( + type = 'stbl', + children = [ + stsd, + stts, + stsc, + stsz, + stco + ]) + ]) + + mdia = DefaultingContainer( + type = 'mdia', + children = [ + DefaultingContainer( + type = 'mdhd', + data = Container( + version = 0, + flags = 0, + creation_time = 0, + modification_time = 0, + timescale = sample_rate, # wtf + duration = frame_duration * len(frames), + language = [ 21, 14, 4 ]), + children = []), + DefaultingContainer( + type = 'hdlr', + data = Container( + version = 0, + flags = 0, + handler_type = 'soun', + name = 'SoundHandler'), + children = []), + + minf + ]) + + trak = DefaultingContainer( + type = 'trak', + children = [ + DefaultingContainer( + type = 'tkhd', + data = Container( + version = 0, + flags = 3, + creation_time = 0, + modification_time = 0, + track_id = 1, + duration = duration, + layer = 0, + alternate_group = 1, + volume = 0x100, + matrix = [ + 0x10000, 0, 0, + 0, 0x10000, 0, + 0, 0, 0x40000000 + ], + width = 0, + height = 0), + children = []), + + mdia + ]) + + moov = DefaultingContainer( + type = 'moov', + children = [ + mvhd, + trak + ]) + + + header = MP4.build([ftyp, moov]) + # print header.encode("hex") + # print len(header) + + # hax + header = header.replace("\xFF\x99\x88\x77", struct.pack(">I", len(header)+8)) + + data = ''.join(frames) + mdat = Box.build(DefaultingContainer( + type = 'mdat', + data_size = len(data), + data = data, + children = [])) + + open("/tmp/tmp.m4a", "a").write(header+mdat) + +def extract_samples(file_name): + f = open(file_name, "rb") + r = MP4.parse_stream(f) + + def find_box(t, n=r): + if isinstance(n, list): + for c in n: + v = find_box(t, c) + if v is not None: return v + return None + + if n.type == t: return n + for c in n.children: + v = find_box(t, c) + if v is not None: return v + return None + + # stts = find_box('stts') + stsc = find_box('stsc') + stco = find_box('stco') + stsz = find_box('stsz') + sample_sizes = stsz.data.sample_size_list.read() + + chunk_samples = [] + for c in stsc.data.sample_entry: + while len(chunk_samples) < c.first_chunk-1: + chunk_samples.append(chunk_samples[-1]) + chunk_samples.append(c.samples_per_chunk) + while len(chunk_samples) < len(stco.data.chunk_offset): + chunk_samples.append(chunk_samples[-1]) + + samples = [] + + j = 0 + for i, s in enumerate(chunk_samples): + offset = stco.data.chunk_offset[i] + for k in xrange(j, j+s): + sample_size = sample_sizes[k] + samples.append(d[offset:offset+sample_size]) + offset += sample_size + j += s + + + return samples diff --git a/mpegts.py b/mpegts.py new file mode 100644 index 0000000..620ac1b --- /dev/null +++ b/mpegts.py @@ -0,0 +1,582 @@ +# mpegts.py +# +# Copyright 2015, espes +# +# Parts adapted from rtmp-livestreaming +# Copyright 2014, Michael Liao +# +# Licensed under GPL Version 3 or later +# + +from construct import * +from collections import defaultdict + +from construct_utils import * + +crc32_tab = [ + 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, + 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, + 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, + 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, + 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, + 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, + 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, + 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, + 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, + 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, + 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, + 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, + 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, + 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, + 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, + 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, + 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, + 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, + 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, + 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, + 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, + 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, + 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, + 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, + 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, + 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, + 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, + 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, + 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, + 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, + 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, + 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, + 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, + 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, + 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, + 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, + 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, + 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, + 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, + 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, + 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, + 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, + 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, + 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, + 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, + 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, + 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, + 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, + 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, + 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, + 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, + 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, + 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, + 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, + 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, + 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, + 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, + 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, + 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, + 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, + 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, + 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, + 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, + 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4] + +def crc32(data): + i_crc = 0xffffffff; + for ch in data: + i_crc = ((i_crc << 8) & 0xffffffff) ^ crc32_tab[(i_crc >> 24) ^ ord(ch)] + return i_crc + + +# ISO/IEC 13818-1:2013 +# Generic coding of moving pictures and associated audio information + +# 2.6 +descriptor = Struct("descriptor", + UBInt8("descriptor_tag"), + UBInt8("descriptor_length"), + String("data", this.descriptor_length)) + # Switch("data", this.descriptor_tag, { + + # }, default=Bork()) + +# 2.4.3.4 +adaptation_field = Struct("adaptation_field", + UBInt8("adaptation_field_length"), + Anchor("_field_start"), + If(lambda ctx: ctx.adaptation_field_length > 0, + Embed(Struct(None, + EmbeddedBitStruct( + Flag("disconuity_indicator"), + Flag("random_access_indicator"), + Flag("elementary_stream_priority_indicator"), + Flag("PCR_flag"), + Flag("OPCR_flag"), + Flag("splicing_point_flag"), + Flag("transport_private_data_flag"), + Flag("adaptation_field_extension_flag")), + If(this.PCR_flag, BitStruct("PCR", + Bits("program_clock_reference_base", 33), + Padding(6), + Bits("program_clock_reference_extension", 9))), + If(this.OPCR_flag, BitStruct("OPCR", + Bits("original_program_clock_reference_base", 33), + Padding(6), + Bits("original_program_clock_reference_extension", 9))), + If(this.splicing_point_flag, UBInt8("splice_countdown")), + # Probe(show_stack=False), + If(this.transport_private_data_flag, Bork("transport_private_data")), + If(this.adaptation_field_extension_flag, Bork("adaptation_field_extension")), + + Anchor("_field_end"), + # stuffing + Padding(lambda ctx: ctx.adaptation_field_length - (ctx._field_end - ctx._field_start), + "\xFF", True)))) + ) + + +# 2.4.3.2 + +transport_packet_length = 188 +transport_packet_header = Struct("transport_packet_header", + Magic("\x47"), #sync_byte + EmbeddedBitStruct( + Flag("transport_error_indicator"), + Flag("payload_unit_start_indicator"), + Flag("transport_priority"), + Bits("PID", 13), + Bits("transport_scrambling_control", 2), + Bits("adaptation_field_control", 2), + Bits("continuity_counter", 4)), + + If(lambda ctx: ctx.adaptation_field_control == 0, Bork()), + + If(lambda ctx: ctx.adaptation_field_control & 0b10, + adaptation_field)) + +transport_packet = Struct("transport_packet", + Anchor("_header_start"), + Embed(transport_packet_header), + Anchor("_header_end"), + If(lambda ctx: ctx.adaptation_field_control & 0b01, + String("data", + lambda ctx: transport_packet_length - (ctx._header_end - ctx._header_start))) + ) + + +MPEG_transport_stream = GreedyRange(transport_packet) + + +def section(name, table_id, *subcons): + return Struct(name, + Padding(1), #pointer_field ........... ??? + Anchor("_crc_start"), + Embed(StructWithLengthAdapter("section", + StructLengthAdapter( + EmbeddedBitStruct( + Const(Bits("table_id", 8), table_id), + Flag("section_syntax_indicator"), + Magic("\x00"), + Padding(2, "\x01"), + Bits("section_length", 12)), + decoder = lambda obj, ctx: obj.section_length-4, + encoder = lambda length, obj, ctx: container_add(obj, section_length=length+4) + ), + Embed(Struct(None, *subcons)) + )), + Anchor("_crc_end"), + + Hash(UBInt32("CRC_32"), + this._crc_start, + lambda ctx: ctx._crc_end - ctx._crc_start, + lambda data, ctx: crc32(data)) + ) + +# 2.4.4.3 +# PAT +program_association_section = section("program_association_section", 0, + EmbeddedBitStruct( + Bits("transport_stream_id", 16), + Padding(2, "\x01"), + Bits("version_number", 5), + Flag("current_next_indicator"), + Bits("section_number", 8), + Bits("last_section_number", 8)), + + OptionalGreedyRange(BitStruct("maps", + Bits("program_number", 16), + Padding(3, "\x01"), + Bits("program_map_PID", 13)))) + +# 2.4.4.8 +# PMT +TS_program_map_section = section("TS_program_map_section", 2, + EmbeddedBitStruct( + Bits("program_number", 16), + Padding(2, "\x01"), + Bits("version_number", 5), + Flag("current_next_indicator"), + Bits("section_number", 8), + Bits("last_section_number", 8), + Padding(3, "\x01"), + Bits("PCR_PID", 13), + Padding(4, "\x01"), + Bits("program_info_length", 12)), + + String("program_info", this.program_info_length), + + OptionalGreedyRange(Struct("maps", + EmbeddedBitStruct( + Bits("stream_type", 8), + Padding(3, "\x01"), + Bits("elementary_PID", 13), + Padding(4, "\x01"), + Bits("ES_info_length", 12)), + String("ES_info", this.ES_info_length))), +) + + + +# 2.4.3.6 + +# Table 2-22 - Stream_id assignments + +PES_STREAM_ID_PROGRAM_STREAM_MAP = 0b10111100 +PES_STREAM_ID_PRIVATE_STREAM_1 = 0b10111101 +PES_STREAM_ID_PADDING_STREAM = 0b10111110 +PES_STREAM_ID_PRIVATE_STREAM_2 = 0b10111111 +PES_STREAM_ID_ECM = 0b11110000 +PES_STREAM_ID_EMM = 0b11110001 +PES_STREAM_ID_PROGRAM_STREAM_DIRECTORY = 0b11111111 +PES_STREAM_ID_DSMCC_STREAM = 0b11110010 +PES_STREAM_ID_ITU_T_REC_H_222_1_TYPE_E_STREAM = 0b11111000 + +PES_HEADERLESS_STREAMS = ( + PES_STREAM_ID_PROGRAM_STREAM_MAP, + PES_STREAM_ID_PRIVATE_STREAM_1, + PES_STREAM_ID_PADDING_STREAM, + PES_STREAM_ID_PRIVATE_STREAM_2, + PES_STREAM_ID_ECM, + PES_STREAM_ID_EMM, + PES_STREAM_ID_PROGRAM_STREAM_DIRECTORY, + PES_STREAM_ID_DSMCC_STREAM, + PES_STREAM_ID_ITU_T_REC_H_222_1_TYPE_E_STREAM) + + +def Split32(name): + return ExprAdapter(Struct(name, + Bits("v0", 3), + Magic("\x01"), + Bits("v1", 15), + Magic("\x01"), + Bits("v2", 15), + Magic("\x01")), + decoder = lambda obj, ctx: (obj.v0 << 30) | (obj.v1 << 15) | obj.v2, + encoder = lambda obj, ctx: Container( + v0 = obj >> 30, + v1 = (obj >> 15) & 0x7fff, + v2 = obj & 0x7fff) + ) + +# Table 2-21 - PES packet +PES_packet = Struct("PES_packet", + Magic("\x00\x00\x01"), #packet_start_code_prefix + UBInt8("stream_id"), + Embed(StructWithLengthAdapter(None, + StructLengthAdapter( + UBInt16("PES_packet_length"), + encoder = lambda length, obj, ctx: 0, #length if length < 65536 else 0, + decoder = lambda obj, con: obj if obj != 0 else 1000000000), + + # Anchor("header_start"), + If(lambda ctx: ctx.stream_id not in PES_HEADERLESS_STREAMS, + Struct("header", + EmbeddedBitStruct( + Magic("\x01\x00"), + Bits("PES_scrambling_control", 2), + Bits("PES_priority", 1), + Flag("data_alignment_indicator"), + Flag("copyright"), + Flag("original_or_copy"), + Bits("PTS_DTS_flags", 2), + Flag("ESCR_flag"), + Flag("ES_rate_flag"), + Flag("DSM_trick_mode_flag"), + Flag("additional_copy_info_flag"), + Flag("PES_CRC_flag"), + Flag("PES_extension_flag")), + Embed(StructWithLength(None, + UBInt8("PES_header_data_length"), + + If(lambda ctx: ctx.PTS_DTS_flags == 0b10, + EmbeddedBitStruct( + Magic("\x00\x00\x01\x00"), + Split32("PTS"))), + If(lambda ctx: ctx.PTS_DTS_flags == 0b11, + EmbeddedBitStruct( + Magic("\x00\x00\x01\x01"), + Split32("PTS"), + Magic("\x00\x00\x00\x01"), + Split32("DTS"))), + If(this.ESCR_flag, Bork("ESCR")), + If(this.ES_rate_flag, Bork("ES_rate")), + If(this.DSM_trick_mode_flag, Bork("DSM_trick")), + If(this.additional_copy_info_flag, Bork("additional_copy_info")), + If(this.PES_CRC_flag, Bork("PES_CRC")), + If(this.PES_extension_flag, Bork("PES_extension")), + StringAdapter(OptionalGreedyRange(Field("stuffing", 1))) + )) + )), + # Anchor("header_end"), + # Probe(show_stack=False), + # IfThenElse("data", lambda ctx: ctx.PES_packet_length != 0, + # String("data", lambda ctx: ctx.PES_packet_length - (ctx.header_end - ctx.header_start)), + # StringAdapter(OptionalGreedyRange(Field("data", 1))) + # ) + # ) + StringAdapter(OptionalGreedyRange(Field("data", 1))) + )) +) + + + +class TSMuxer(object): + def __init__(self, f, has_vid_stream, has_aud_stream): + self.f = f + self.has_vid_stream = has_vid_stream + self.has_aud_stream = has_aud_stream + + assert self.has_vid_stream or self.has_aud_stream + + self.program_number = 1 + self.pmt_pid = 4096 + + self.vid_es_pid = 256 + self.aud_es_pid = 267 + + self.pcr_pid = self.vid_es_pid if self.has_vid_stream else self.self.aud_es_pid + + self.vid_stream_type = 0x1B # AVC video stream + self.aud_stream_type = 0x11 # ACC audio with LATM transport + + self.pts_clock = 90000 # hz + + self.continuity_counters = defaultdict(int) + + def write_tables(self): + pat = DefaultingContainer( + table_id = 0, + section_syntax_indicator = True, + transport_stream_id = 1, + version_number = 22, + current_next_indicator = True, + section_number = 0, + last_section_number = 0, + maps = [ + Container( + program_number = self.program_number, + program_map_PID = self.pmt_pid) + ]) + pat_data = program_association_section.build(pat) + # print pat_data.encode("hex") + + pat_packet = DefaultingContainer( + transport_error_indicator = False, + payload_unit_start_indicator = True, + transport_priority = False, + PID = 0, + transport_scrambling_control = 0, + adaptation_field_control = 1, + continuity_counter = self.continuity_counters[0] % 16, + adaptation_field = None + ) + pat_packet_header = transport_packet_header.build(pat_packet) + pat_packet_data = (pat_packet_header + pat_data).ljust(transport_packet_length, "\xFF") + + + self.f.write(pat_packet_data) + self.continuity_counters[0] += 1 + + maps = [] + if self.has_vid_stream: maps.append((self.vid_stream_type, self.vid_es_pid)) + if self.has_aud_stream: maps.append((self.aud_stream_type, self.aud_es_pid)) + + pmt = DefaultingContainer( + table_id = 2, + section_syntax_indicator = True, + program_number = self.program_number, + version_number = 0, + current_next_indicator = True, + section_number = 0, + last_section_number = 0, + PCR_PID = self.pcr_pid, + program_info_length = 0, + program_info = '', + maps = [ + Container( + stream_type = t, + elementary_PID = pid, + ES_info_length = 0, + ES_info = '') for t, pid in maps + ], + ) + pmt_data = TS_program_map_section.build(pmt) + + pmt_packet = DefaultingContainer( + transport_error_indicator = False, + payload_unit_start_indicator = True, + transport_priority = False, + PID = self.pmt_pid, + transport_scrambling_control = 0, + adaptation_field_control = 1, + continuity_counter = self.continuity_counters[self.pmt_pid] % 16, + adaptation_field = None, + # data = pmt_data.ljust(184, "\xFF") + ) + pmt_packet_header = transport_packet_header.build(pmt_packet) + pmt_packet_data = (pmt_packet_header + pmt_data).ljust(transport_packet_length, "\xFF") + + + self.f.write(pmt_packet_data) + self.continuity_counters[self.pmt_pid] += 1 + + + def mux_h264(self, t, data): + pts = int(t * self.pts_clock) + + pes = DefaultingContainer( + stream_id = 0b11100000, # video stream number 0 + header = DefaultingContainer( + PES_scrambling_control = 0, + PES_priority = 0, + data_alignment_indicator = False, + copyright = False, + original_or_copy = False, + PTS_DTS_flags = 2, # 3 if DTS else 2 + ESCR_flag = False, + ES_rate_flag = False, + DSM_trick_mode_flag = False, + additional_copy_info_flag = False, + PES_CRC_flag = False, + PES_extension_flag = False, + PTS = pts, + # DTS = t, # wut + stuffing = ''), + data = data) + + pes_data = PES_packet.build(pes) + + self.write_pes_data(self.vid_es_pid, pes_data, pts) + + def mux_latm(self, t, data): + pts = int(t * self.pts_clock) + + pes = DefaultingContainer( + stream_id = 0b11000001, # audio stream number 1 + header = DefaultingContainer( + PES_scrambling_control = 0, + PES_priority = 0, + data_alignment_indicator = True, + copyright = False, + original_or_copy = False, + PTS_DTS_flags = 2, # 3 if DTS else 2 + ESCR_flag = False, + ES_rate_flag = False, + DSM_trick_mode_flag = False, + additional_copy_info_flag = False, + PES_CRC_flag = False, + PES_extension_flag = False, + PTS = pts, + stuffing = ''), + data = data) + + pes_data = PES_packet.build(pes) + + self.write_pes_data(self.aud_es_pid, pes_data, pts) + + def write_pes_data(self, pid, data, ts=None): + pcr = ts if pid == self.pcr_pid else None + + first_packet = True + while len(data): + pcr_v = None + if first_packet and pcr is not None: + pcr_v = Container(program_clock_reference_base = pcr, program_clock_reference_extension = 0) + + field = None + if pcr_v: + field_length = 8 + field = DefaultingContainer( + adaptation_field_length = field_length - 1, + disconuity_indicator = False, + random_access_indicator = False, + elementary_stream_priority_indicator = False, + PCR_flag = True, + OPCR_flag = False, + splicing_point_flag = False, + transport_private_data_flag = False, + adaptation_field_extension_flag = False, + PCR = pcr_v) + + header_data = transport_packet_header.build( + DefaultingContainer( + transport_error_indicator = False, + payload_unit_start_indicator = first_packet, + transport_priority = False, + PID = pid, + transport_scrambling_control = 0, + adaptation_field_control = 3 if field else 1, + continuity_counter = self.continuity_counters[pid] % 16, + adaptation_field = field + ) + ) + + data_slice = data[:transport_packet_length - len(header_data)] + data = data[len(data_slice):] + + packet = header_data + data_slice + if len(packet) < transport_packet_length: + assert len(data) == 0 + # last packet, pad it + # pad with an adaptation_field when the pes data length is not given... + # (otherwise we /could/ pad just pad the data with \xFF...) + padding_needed = transport_packet_length - len(packet) + if field: + field.adaptation_field_length += padding_needed + else: + assert pcr_v is None + if padding_needed == 1: + field = DefaultingContainer(adaptation_field_length = 0) + else: + field = DefaultingContainer( + adaptation_field_length = padding_needed-1, + disconuity_indicator = False, + random_access_indicator = False, + elementary_stream_priority_indicator = False, + PCR_flag = False, + OPCR_flag = False, + splicing_point_flag = False, + transport_private_data_flag = False, + adaptation_field_extension_flag = False, + ) + + header_data = transport_packet_header.build( + DefaultingContainer( + transport_error_indicator = False, + payload_unit_start_indicator = first_packet, + transport_priority = False, + PID = pid, + transport_scrambling_control = 0, + adaptation_field_control = 3, + continuity_counter = self.continuity_counters[pid] % 16, + adaptation_field = field + ) + ) + packet = header_data + data_slice + assert len(packet) == transport_packet_length + + self.f.write(packet) + self.continuity_counters[pid] += 1 + first_packet = False + + + +