├── ioscreen ├── __init__.py ├── coremedia │ ├── __init__.py │ ├── common.py │ ├── CMTime.py │ ├── AudioStream.py │ ├── CMclock.py │ ├── wav.py │ ├── consumer.py │ ├── CMFormatDescription.py │ ├── serialize.py │ ├── CMSampleBuffer.py │ └── gstreamer.py ├── ping.py ├── main.py ├── util.py ├── asyn.py ├── meaasge.py └── sync.py ├── test_case ├── fixtures │ ├── asyn-hpd0 │ ├── cwpa-reply2 │ ├── bulvalue.bin │ ├── asyn-eat │ ├── dict.bin │ ├── og-reply │ ├── rply.bin │ ├── afmt-reply │ ├── asyn-feed │ ├── asyn-hpa0 │ ├── asyn-hpa1 │ ├── asyn-hpd1 │ ├── asyn-need │ ├── asyn-rels │ ├── asyn-sprp │ ├── asyn-sprp2 │ ├── asyn-srat │ ├── asyn-tbas │ ├── asyn-tjmp │ ├── clok-reply │ ├── cvrp-reply │ ├── cwpa-reply1 │ ├── intdict.bin │ ├── og-request │ ├── skew-reply │ ├── stop-reply │ ├── time-reply1 │ ├── afmt-request │ ├── clok-request │ ├── cvrp-request │ ├── cwpa-request1 │ ├── cwpa-request2 │ ├── skew-request │ ├── stop-request │ ├── time-request1 │ ├── adsb-from-fdsc │ ├── asyn-eat-nofdsc │ ├── asyn-feed-nofdsc │ ├── complex_dict.bin │ ├── asyn-feed-unknown1 │ ├── serialize_dict.bin │ ├── asyn-feed-ttas-only │ ├── formatdescriptor.bin │ ├── adsb-from-hpa-dict.bin │ └── formatdescriptor-audio.bin ├── test_audio_stream.py ├── test_cmclock.py ├── test_wav.py ├── test_format_descriptor.py ├── test_asyn.py ├── test_serialize.py ├── test_sync.py └── test_sample_buffer.py ├── requirements.txt ├── setup.py ├── pypi.sh ├── setup.cfg ├── README.md ├── LICENSE └── windows.md /ioscreen/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ioscreen/coremedia/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_case/fixtures/asyn-hpd0: -------------------------------------------------------------------------------- 1 | nysa0dph -------------------------------------------------------------------------------- /test_case/fixtures/cwpa-reply2: -------------------------------------------------------------------------------- 1 | ylpr@^QF෢ -------------------------------------------------------------------------------- /test_case/fixtures/bulvalue.bin: -------------------------------------------------------------------------------- 1 | (tcid vyekkrtsValeria vlub -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyGObject==3.38.0 2 | PyGObject-stubs==0.0.2 3 | pyusb==1.1.1 4 | 5 | -------------------------------------------------------------------------------- /test_case/fixtures/asyn-eat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-eat -------------------------------------------------------------------------------- /test_case/fixtures/dict.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/dict.bin -------------------------------------------------------------------------------- /test_case/fixtures/og-reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/og-reply -------------------------------------------------------------------------------- /test_case/fixtures/rply.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/rply.bin -------------------------------------------------------------------------------- /test_case/fixtures/afmt-reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/afmt-reply -------------------------------------------------------------------------------- /test_case/fixtures/asyn-feed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-feed -------------------------------------------------------------------------------- /test_case/fixtures/asyn-hpa0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-hpa0 -------------------------------------------------------------------------------- /test_case/fixtures/asyn-hpa1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-hpa1 -------------------------------------------------------------------------------- /test_case/fixtures/asyn-hpd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-hpd1 -------------------------------------------------------------------------------- /test_case/fixtures/asyn-need: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-need -------------------------------------------------------------------------------- /test_case/fixtures/asyn-rels: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-rels -------------------------------------------------------------------------------- /test_case/fixtures/asyn-sprp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-sprp -------------------------------------------------------------------------------- /test_case/fixtures/asyn-sprp2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-sprp2 -------------------------------------------------------------------------------- /test_case/fixtures/asyn-srat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-srat -------------------------------------------------------------------------------- /test_case/fixtures/asyn-tbas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-tbas -------------------------------------------------------------------------------- /test_case/fixtures/asyn-tjmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-tjmp -------------------------------------------------------------------------------- /test_case/fixtures/clok-reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/clok-reply -------------------------------------------------------------------------------- /test_case/fixtures/cvrp-reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/cvrp-reply -------------------------------------------------------------------------------- /test_case/fixtures/cwpa-reply1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/cwpa-reply1 -------------------------------------------------------------------------------- /test_case/fixtures/intdict.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/intdict.bin -------------------------------------------------------------------------------- /test_case/fixtures/og-request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/og-request -------------------------------------------------------------------------------- /test_case/fixtures/skew-reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/skew-reply -------------------------------------------------------------------------------- /test_case/fixtures/stop-reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/stop-reply -------------------------------------------------------------------------------- /test_case/fixtures/time-reply1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/time-reply1 -------------------------------------------------------------------------------- /test_case/fixtures/afmt-request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/afmt-request -------------------------------------------------------------------------------- /test_case/fixtures/clok-request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/clok-request -------------------------------------------------------------------------------- /test_case/fixtures/cvrp-request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/cvrp-request -------------------------------------------------------------------------------- /test_case/fixtures/cwpa-request1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/cwpa-request1 -------------------------------------------------------------------------------- /test_case/fixtures/cwpa-request2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/cwpa-request2 -------------------------------------------------------------------------------- /test_case/fixtures/skew-request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/skew-request -------------------------------------------------------------------------------- /test_case/fixtures/stop-request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/stop-request -------------------------------------------------------------------------------- /test_case/fixtures/time-request1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/time-request1 -------------------------------------------------------------------------------- /test_case/fixtures/adsb-from-fdsc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/adsb-from-fdsc -------------------------------------------------------------------------------- /test_case/fixtures/asyn-eat-nofdsc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-eat-nofdsc -------------------------------------------------------------------------------- /test_case/fixtures/asyn-feed-nofdsc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-feed-nofdsc -------------------------------------------------------------------------------- /test_case/fixtures/complex_dict.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/complex_dict.bin -------------------------------------------------------------------------------- /test_case/fixtures/asyn-feed-unknown1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-feed-unknown1 -------------------------------------------------------------------------------- /test_case/fixtures/serialize_dict.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/serialize_dict.bin -------------------------------------------------------------------------------- /test_case/fixtures/asyn-feed-ttas-only: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/asyn-feed-ttas-only -------------------------------------------------------------------------------- /test_case/fixtures/formatdescriptor.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/formatdescriptor.bin -------------------------------------------------------------------------------- /test_case/fixtures/adsb-from-hpa-dict.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/adsb-from-hpa-dict.bin -------------------------------------------------------------------------------- /test_case/fixtures/formatdescriptor-audio.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YueChen-C/ios-screen-record/HEAD/test_case/fixtures/formatdescriptor-audio.bin -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import setuptools 4 | 5 | 6 | setuptools.setup( 7 | setup_requires=['pbr'], pbr=True, python_requires=">=3.7") 8 | -------------------------------------------------------------------------------- /pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | 4 | set -e 5 | 6 | rm -fr dist/ build/ 7 | python3 setup.py sdist bdist_wheel 8 | 9 | export TWINE_REPOSITORY_URL=https://upload.pypi.org/legacy/ 10 | export TWINE_USERNAME=chenpeijie 11 | export TWINE_PASSWORD= 12 | 13 | exec twine upload dist/* 14 | -------------------------------------------------------------------------------- /test_case/test_audio_stream.py: -------------------------------------------------------------------------------- 1 | from ioscreen.coremedia.AudioStream import AudioStreamBasicDescription 2 | 3 | 4 | def test_audio_stream_serializer(): 5 | with open('./fixtures/adsb-from-hpa-dict.bin', "rb") as f: 6 | data = f.read() 7 | 8 | adsb = AudioStreamBasicDescription.new() 9 | buf = adsb.to_bytes() 10 | assert data == buf 11 | parsedAdsb = AudioStreamBasicDescription.from_buffer_copy(buf) 12 | assert str(adsb) == str(parsedAdsb) 13 | -------------------------------------------------------------------------------- /test_case/test_cmclock.py: -------------------------------------------------------------------------------- 1 | from ioscreen.coremedia.CMclock import CMClock, NanoSecondScale 2 | 3 | 4 | def test_clock_get_time(): 5 | cm_clock = CMClock.new(5) 6 | assert NanoSecondScale == cm_clock.timeScale 7 | time1 = cm_clock.getTime() 8 | time2 = cm_clock.getTime() 9 | assert cm_clock.timeScale == time1.CMTimeScale 10 | assert time2.CMTimeValue > time1.CMTimeValue 11 | cm_clock = CMClock.new_scale(0, 1) 12 | assert 0 == cm_clock.getTime().CMTimeValue 13 | -------------------------------------------------------------------------------- /ioscreen/ping.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import struct 3 | 4 | 5 | class PingConst(enum.IntEnum): 6 | PingPacketMagic = 0x70696E67 7 | PingLength = 16 8 | PingHeader = 0x0000000100000000 9 | 10 | 11 | def new_ping_packet_bytes(): 12 | """default Ping ioscreen 13 | :return: 14 | """ 15 | packet_bytes = b'' 16 | packet_bytes += struct.pack('=3.7 17 | 18 | 1. `brew install libusb pkg-config` 19 | 2. 如需使用 gstreamer 则需要安装 `brew install gstreamer gst-plugins-bad gst-plugins-good gst-plugins-base gst-plugins-ugly` 20 | 2. `pip install ioscreen` 21 | 22 | 23 | 24 | #### 使用 25 | ```bash 26 | # vlc 工具播放udp地址: udp/h264://@:8880 27 | # 转发 h264 udp 广播,Mac 本身限制了 udp 发送,延迟会越来越大(仅测试使用) 28 | $ ioscreen --udid=xxxx udp 29 | 30 | # 录制 h264/wav 文件 31 | $ ioscreen --udid=xxxx record -h264File=/home/out.h264 -wavFile=/home/out.wav 32 | 33 | # gstreamer 渲染显示画面 34 | $ ioscreen --udid=xxxx gstreamer 35 | ``` 36 | 37 | QQ 交流群:37042417 38 | 39 | -------------------------------------------------------------------------------- /test_case/test_format_descriptor.py: -------------------------------------------------------------------------------- 1 | from ioscreen.coremedia.CMFormatDescription import FormatDescriptor, DescriptorConst 2 | 3 | ppsHex = "27640033AC5680470133E69E6E04040404" 4 | spsHex = "28EE3CB0" 5 | 6 | 7 | def test_format_descriptor(): 8 | with open('./fixtures/formatdescriptor.bin', "rb") as f: 9 | data = f.read() 10 | 11 | fdsc = FormatDescriptor.from_bytes(data) 12 | assert DescriptorConst.MediaTypeVideo == fdsc.MediaType 13 | assert bytes.fromhex(ppsHex) == fdsc.PPS 14 | assert bytes.fromhex(spsHex) == fdsc.SPS 15 | print(fdsc) 16 | 17 | 18 | def test_format_descriptor_audio(): 19 | with open('./fixtures/formatdescriptor-audio.bin', "rb") as f: 20 | data = f.read() 21 | fdsc = FormatDescriptor.from_bytes(data) 22 | assert DescriptorConst.MediaTypeSound == fdsc.MediaType 23 | print(fdsc) 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 YueChen 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 | -------------------------------------------------------------------------------- /test_case/test_asyn.py: -------------------------------------------------------------------------------- 1 | from ioscreen.asyn import * 2 | 3 | 4 | def test_feed(): 5 | with open('./fixtures/asyn-feed', "rb") as f: 6 | data = f.read() 7 | BufPacket = AsynCmSampleBufPacket.from_bytes(data[4:]) 8 | print(BufPacket) 9 | 10 | 11 | def test_feed_eat(): 12 | with open('./fixtures/asyn-eat', "rb") as f: 13 | data = f.read() 14 | BufPacket = AsynCmSampleBufPacket.from_bytes(data) 15 | print(BufPacket) 16 | 17 | 18 | def test_rels(): 19 | with open('./fixtures/asyn-rels', "rb") as f: 20 | data = f.read() 21 | BufPacket = AsynRelsPacket.from_bytes(data[4:]) 22 | print(BufPacket) 23 | 24 | 25 | def test_sprp(): 26 | with open('./fixtures/asyn-sprp', "rb") as f: 27 | data = f.read() 28 | BufPacket = AsynSprpPacket.from_bytes(data) 29 | print(BufPacket) 30 | 31 | 32 | def test_srat(): 33 | with open('./fixtures/asyn-srat', "rb") as f: 34 | data = f.read() 35 | BufPacket = AsynSratPacket.from_bytes(data) 36 | print(BufPacket) 37 | 38 | 39 | def test_tbas(): 40 | with open('./fixtures/asyn-tbas', "rb") as f: 41 | data = f.read() 42 | BufPacket = AsynTbasPacket.from_bytes(data) 43 | print(BufPacket) 44 | 45 | def test_tjmp(): 46 | with open('./fixtures/asyn-tjmp', "rb") as f: 47 | data = f.read() 48 | BufPacket = AsynTjmpPacket.from_bytes(data) 49 | print(BufPacket) 50 | 51 | -------------------------------------------------------------------------------- /ioscreen/coremedia/common.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | 4 | class NSNumber: 5 | def __init__(self, typeSpecifier, value): 6 | """ 7 | :param typeSpecifier: 3 uint32, 4: uint64, 6:float64 8 | :param value: 9 | """ 10 | self.typeSpecifier = typeSpecifier 11 | self.value = value 12 | 13 | @classmethod 14 | def from_bytes(cls, buf): 15 | typeSpecifier = buf[0] 16 | if typeSpecifier == 3: 17 | value = struct.unpack('>> typeSpecifier:{self.typeSpecifier},value:{self.value}' 43 | -------------------------------------------------------------------------------- /ioscreen/coremedia/CMTime.py: -------------------------------------------------------------------------------- 1 | # iOS Frameworks 2 | # https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.8.sdk/System/Library/Frameworks/CoreMedia.framework/Versions/A/Headers/CMTime.h 3 | 4 | import enum 5 | import struct 6 | from _ctypes import Structure 7 | from ctypes import c_uint32, c_uint64 8 | 9 | NanoSecondScale = 1000000000 10 | 11 | 12 | class CMTimeConst(enum.IntEnum): 13 | KCMTimeFlagsValid = 0x0 14 | KCMTimeFlagsHasBeenRounded = 0x1 15 | KCMTimeFlagsPositiveInfinity = 0x2 16 | KCMTimeFlagsNegativeInfinity = 0x4 17 | KCMTimeFlagsIndefinite = 0x8 18 | KCMTimeFlagsImpliedValueFlagsMask = KCMTimeFlagsPositiveInfinity | KCMTimeFlagsNegativeInfinity | KCMTimeFlagsIndefinite 19 | CMTimeLengthInBytes = 24 20 | 21 | 22 | class CMTime(Structure): 23 | _fields_ = [ 24 | ('CMTimeValue', c_uint64), 25 | ('CMTimeScale', c_uint32), 26 | ('CMTimeFlags', c_uint32), 27 | ('CMTimeEpoch', c_uint64), 28 | ] 29 | 30 | def get_time_scale(self, newScaleToUse): 31 | scalingFactor = float(newScaleToUse.CMTimeScale) / float(self.CMTimeScale) 32 | return float(self.CMTimeValue) * scalingFactor 33 | 34 | def seconds(self): 35 | if self.CMTimeValue == 0: 36 | return 0 37 | return int(self.CMTimeValue / self.CMTimeScale) 38 | 39 | def __str__(self): 40 | return f"CMTime:{self.CMTimeValue}/{self.CMTimeScale}, flags:{self.CMTimeFlags}, epoch:{self.CMTimeEpoch}" 41 | -------------------------------------------------------------------------------- /ioscreen/coremedia/AudioStream.py: -------------------------------------------------------------------------------- 1 | ## iOS AudioStreamBasicDescription 2 | import struct 3 | from _ctypes import Structure 4 | from ctypes import c_uint32, c_double 5 | 6 | AudioFormatIDLMagic = 0x6C70636D 7 | 8 | 9 | class AudioStreamBasicDescription(Structure): 10 | _fields_ = [ 11 | ('SampleRate', c_double), 12 | ('FormatID', c_uint32), 13 | ('FormatFlags', c_uint32), 14 | ('BytesPerPacket', c_uint32), 15 | ('FramesPerPacket', c_uint32), 16 | ('BytesPerFrame', c_uint32), 17 | ('ChannelsPerFrame', c_uint32), 18 | ('BitsPerChannel', c_uint32), 19 | ('Reserved', c_uint32), 20 | 21 | ] 22 | 23 | def __str__(self): 24 | return f"AudioStreamBasicDescription >> SampleRate:{self.SampleRate},FormatFlags:{self.FormatFlags}" \ 25 | f",BytesPerPacket:{self.BytesPerPacket},FramesPerPacket:{self.FramesPerPacket}," \ 26 | f"BytesPerFrame:{self.BytesPerFrame},ChannelsPerFrame:{self.ChannelsPerFrame}," \ 27 | f"BitsPerChannel:{self.BitsPerChannel},Reserved:{self.Reserved}" 28 | 29 | def to_bytes(self): 30 | buf = bytes(self) + struct.pack('I', 0x64617461) 57 | buf += struct.pack(' AppleMobileDeviceService.exe(⾃带USB驱动) -> iPhone 10 | 2. libimobiledevice -> AppleMobileDeviceService.exe(⾃带USB驱动) -> iPhone 11 | 3. libimobiledevice -> libusbmuxd -> usbmuxd -> libusb-win32 -> iPhone 12 | 13 | 14 | 因为 iOS Screen协议⽆法通过 AppleMobileDeviceService.exe 跟iPhone通讯,所以需要用新的驱动 15 | 通讯⽅案 16 | ### 新方案 17 | 1. 可以使用编译 https://github.com/libimobiledevice-win32/usbmuxd 作为驱动 18 | 2. 也可以使用编译 https://github.com/YueChen-C/usbmuxd 作为驱动(修复了些 bug) 19 | 20 | 编译后得到: 21 | usbmuxd.exe 22 | 23 | 你也可以用编译好的 https://github.com/iFred09/libimobiledevice-windows/archive/master.zip 来验证服务 24 | 25 | #### 执行步骤: 26 | 1. 通过 [zadig](https://zadig.akeo.ie/) 安装libusb-win32驱动 27 | 2. 验证阶段 kill AppleMobileDeviceService.exe 进程,避免⼲扰 usbmuxd.exe 28 | 3. 运⾏ usbmuxd.exe,运⾏成功后会⾃动监听会监听27015端 29 | 4. 执⾏ ideviceinfo.exe,如果有正常显示数据,则说明通讯⽅案跑通,如果提示 No device found 30 | 则说明通讯失败,需要排查下原因 31 | 32 | #### 运⾏ioscreen 33 | 1. 按照上述验证步骤,libusb-win32驱 动已经安装成功 34 | 2. pip install pyusb==1.1.1 35 | 3. git clone https://github.com/YueChen-C/ios-screen-record.git && cd ios-screen_record && git checkout windows && python setup.py install 36 | 4. 通过 ideviceinfo.exe 获取 iPhone 设备的 udid,运⾏ idevicepair.exe -u 获取到的 iPhone 设备的 udid 37 | pair 进⾏配对 38 | 5. 执⾏ ioscreen --udid=获取到的iPhone设备的udid record -h264File=out.h264 - 39 | wavFile=out.wav ,正常情况下会在接收到 PING之后提示libusb写⼊相关的time out,是因为配对 40 | 问题 41 | 6. 修改安装的ioscreen的site-package,在代码执⾏到此处时暂停:https://github.com/YueChen-C/ios-screen-record/blob/06babe5a6452ae9918dbd653d9e6f003063c9489/ioscreen/util.py#L152 42 | ⼏秒后继续执⾏ioscreen 修改QT-Config会导致设备重连,所以这⾥要等待⼏秒 43 | (经测试5秒⽐较稳定)等设备重连成功后继续执⾏,或者使⽤其它⽅式检测是否重连成功后再继续 44 | 执⾏),即可成功将视频流和⾳频流保存到out.h264和out.wav -------------------------------------------------------------------------------- /test_case/test_serialize.py: -------------------------------------------------------------------------------- 1 | from ioscreen.asyn import create_hpa1_device, create_hpd1_device 2 | from ioscreen.coremedia.serialize import SerializeStringKeyDict, new_dictionary_from_bytes, DictConst, \ 3 | new_string_dict_from_bytes 4 | 5 | 6 | def test_BooleanSerialization(): 7 | with open('./fixtures/bulvalue.bin', "rb") as f: 8 | data = f.read() 9 | serializedDict = SerializeStringKeyDict({'Valeria': True}) 10 | assert data == serializedDict.to_bytes() 11 | 12 | 13 | def test_FullSerialization(): 14 | with open('./fixtures/serialize_dict.bin', "rb") as f: 15 | data = f.read() 16 | serializedBytes = SerializeStringKeyDict(create_hpa1_device()) 17 | assert data == serializedBytes.to_bytes() 18 | 19 | with open('./fixtures/dict.bin', "rb") as f: 20 | data = f.read() 21 | serializedBytes = SerializeStringKeyDict(create_hpd1_device()) 22 | assert data == serializedBytes.to_bytes() 23 | 24 | 25 | def test_IntDict(): 26 | with open('./fixtures/intdict.bin', "rb") as f: 27 | data = f.read() 28 | 29 | mydict = new_dictionary_from_bytes(data, DictConst.DictionaryMagic) 30 | assert 2 == len(mydict) 31 | print(mydict) 32 | 33 | 34 | def test_BooleanEntry(): 35 | with open('./fixtures/bulvalue.bin', "rb") as f: 36 | data = f.read() 37 | mydict = new_string_dict_from_bytes(data) 38 | assert 1 == len(mydict) 39 | print(mydict) 40 | 41 | 42 | def test_SimpleDict(): 43 | with open('./fixtures/dict.bin', "rb") as f: 44 | data = f.read() 45 | 46 | mydict = new_string_dict_from_bytes(data) 47 | assert 3 == len(mydict) 48 | assert 1920.0 == mydict.get('DisplaySize').get('Width').value 49 | assert 1200.0 == mydict.get('DisplaySize').get('Height').value 50 | print(mydict) 51 | 52 | 53 | def test_ComplexDict(): 54 | with open('./fixtures/complex_dict.bin', "rb") as f: 55 | data = f.read() 56 | mydict = new_string_dict_from_bytes(data) 57 | assert 3 == len(mydict) 58 | print(mydict) 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /ioscreen/main.py: -------------------------------------------------------------------------------- 1 | import _thread 2 | import argparse 3 | 4 | from ioscreen.util import * 5 | 6 | 7 | def cmd_record_wav(args: argparse.Namespace): 8 | device = find_ios_device(args.udid) 9 | consumer = AVFileWriter(h264FilePath=args.h264File, wavFilePath=args.wavFile) 10 | stopSignal = threading.Event() 11 | register_signal(stopSignal) 12 | start_reading(consumer, device, stopSignal) 13 | 14 | 15 | def cmd_record_udp(args: argparse.Namespace): 16 | device = find_ios_device(args.udid) 17 | consumer = SocketUDP() 18 | stopSignal = threading.Event() 19 | register_signal(stopSignal) 20 | start_reading(consumer, device, stopSignal) 21 | 22 | 23 | def cmd_record_gstreamer(args: argparse.Namespace): 24 | from ioscreen.coremedia.gstreamer import GstAdapter 25 | device = find_ios_device(args.udid) 26 | stopSignal = threading.Event() 27 | register_signal(stopSignal) 28 | consumer = GstAdapter.new(stopSignal) 29 | _thread.start_new_thread(start_reading, (consumer, device, stopSignal,)) 30 | consumer.loop.run() 31 | 32 | 33 | def main(): 34 | parser = argparse.ArgumentParser() 35 | subparsers = parser.add_subparsers(dest='subparser') 36 | parser.add_argument("-u", "--udid", help="specify unique device identifier") 37 | gstreamer_parser = subparsers.add_parser("gstreamer", 38 | help="record will open a new window and push AV data to gstreamer.") 39 | gstreamer_parser.set_defaults(func=cmd_record_gstreamer) 40 | 41 | udp_parser = subparsers.add_parser("udp", 42 | help="forward H264 data to UDP broadcast. You can use VLC to play the URL") 43 | udp_parser.set_defaults(func=cmd_record_udp) 44 | 45 | parser_foo = subparsers.add_parser('record', help="will start video&audio recording. Video will be saved in a raw " 46 | "h264 file playable by VLC") 47 | parser_foo.add_argument('-h264File', type=str, required=True, 48 | help='lease specify a valid path like /home/test/out.h264') 49 | parser_foo.add_argument('-wavFile', type=str, required=True, 50 | help='lease specify a valid path like /home/test/out.wav') 51 | parser_foo.set_defaults(func=cmd_record_wav) 52 | args = parser.parse_args() 53 | if not args.subparser: 54 | parser.print_help() 55 | return 56 | args.func(args) 57 | 58 | 59 | if __name__ == '__main__': 60 | main() 61 | -------------------------------------------------------------------------------- /test_case/test_sync.py: -------------------------------------------------------------------------------- 1 | from ioscreen.coremedia.CMTime import CMTimeConst 2 | from ioscreen.coremedia.CMTime import CMTime as CM 3 | from ioscreen.sync import * 4 | 5 | 6 | def test_afmt(): 7 | with open('./fixtures/afmt-request', "rb") as f: 8 | data = f.read() 9 | BufPacket = SyncAfmtPacket.from_bytes(data[4:]) 10 | print(BufPacket) 11 | Bytes = BufPacket.to_bytes() 12 | with open('./fixtures/afmt-reply', "rb") as f: 13 | data = f.read() 14 | assert data == Bytes 15 | 16 | 17 | def test_clock(): 18 | with open('./fixtures/clok-request', "rb") as f: 19 | data = f.read() 20 | BufPacket = SyncClockPacket.from_bytes(data[4:]) 21 | print(BufPacket) 22 | Bytes = BufPacket.to_bytes(0x00007FA67CC17980) 23 | with open('./fixtures/clok-reply', "rb") as f: 24 | data = f.read() 25 | assert data == Bytes 26 | 27 | 28 | def test_cvrp(): 29 | with open('./fixtures/cvrp-request', "rb") as f: 30 | data = f.read() 31 | BufPacket = SyncCvrpPacket.from_bytes(data[4:]) 32 | print(BufPacket) 33 | Bytes = BufPacket.to_bytes(0x00007FA66CD10250) 34 | with open('./fixtures/cvrp-reply', "rb") as f: 35 | data = f.read() 36 | assert data == Bytes 37 | 38 | 39 | def test_cwpa(): 40 | with open('./fixtures/cwpa-request1', "rb") as f: 41 | data = f.read() 42 | BufPacket = SyncCwpaPacket.from_bytes(data[4:]) 43 | print(BufPacket) 44 | Bytes = BufPacket.to_bytes(0x00007FA66CE20CB0) 45 | with open('./fixtures/cwpa-reply1', "rb") as f: 46 | data = f.read() 47 | assert data == Bytes 48 | 49 | 50 | def test_og(): 51 | with open('./fixtures/og-request', "rb") as f: 52 | data = f.read() 53 | BufPacket = SyncOGPacket.from_bytes(data[4:]) 54 | print(BufPacket) 55 | Bytes = BufPacket.to_bytes() 56 | with open('./fixtures/og-reply', "rb") as f: 57 | data = f.read() 58 | assert data == Bytes 59 | 60 | 61 | def test_skew(): 62 | with open('./fixtures/skew-request', "rb") as f: 63 | data = f.read() 64 | BufPacket = SyncSkewPacket.from_bytes(data[4:]) 65 | print(BufPacket) 66 | Bytes = BufPacket.to_bytes(48000) 67 | with open('./fixtures/skew-reply', "rb") as f: 68 | data = f.read() 69 | assert data == Bytes 70 | 71 | 72 | def test_stop(): 73 | with open('./fixtures/stop-request', "rb") as f: 74 | data = f.read() 75 | BufPacket = SyncStopPacket.from_bytes(data[4:]) 76 | print(BufPacket) 77 | Bytes = BufPacket.to_bytes() 78 | with open('./fixtures/stop-reply', "rb") as f: 79 | data = f.read() 80 | assert data == Bytes 81 | 82 | 83 | def test_time(): 84 | with open('./fixtures/time-request1', "rb") as f: 85 | data = f.read() 86 | BufPacket = SyncTimePacket.from_bytes(data[4:]) 87 | print(BufPacket) 88 | _time = CM( 89 | CMTimeValue=0x0000BA62C442E1E1, 90 | CMTimeScale=0x3B9ACA00, 91 | CMTimeFlags=CMTimeConst.KCMTimeFlagsHasBeenRounded, 92 | CMTimeEpoch=0) 93 | Bytes = BufPacket.to_bytes(_time) 94 | with open('./fixtures/time-reply1', "rb") as f: 95 | data = f.read() 96 | assert data == Bytes 97 | -------------------------------------------------------------------------------- /test_case/test_sample_buffer.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | from ioscreen.coremedia.CMSampleBuffer import CMSampleBuffer 4 | from ioscreen.coremedia.CMTime import CMTimeConst 5 | 6 | 7 | def test_feed_data(): 8 | with open('./fixtures/asyn-feed-ttas-only', "rb") as f: 9 | data = f.read() 10 | sbufPacket = CMSampleBuffer.from_bytesVideo(data[20:]) 11 | assert False == sbufPacket.HasFormatDescription 12 | print(sbufPacket) 13 | 14 | 15 | def test_CMSampleBuffer(): 16 | with open('./fixtures/asyn-feed', "rb") as f: 17 | data = f.read() 18 | sbufPacket = CMSampleBuffer.from_bytesVideo(data[20:]) 19 | assert True == sbufPacket.HasFormatDescription 20 | assert CMTimeConst.KCMTimeFlagsHasBeenRounded == sbufPacket.OutputPresentationTimestamp.CMTimeFlags 21 | assert 0x176a7 == sbufPacket.OutputPresentationTimestamp.seconds() 22 | assert 1 == len(sbufPacket.SampleTimingInfoArray) 23 | assert 0 == sbufPacket.SampleTimingInfoArray[0].Duration.seconds() 24 | assert 0x176a7 == sbufPacket.SampleTimingInfoArray[0].PresentationTimeStamp.seconds() 25 | assert 0 == sbufPacket.SampleTimingInfoArray[0].DecodeTimeStamp.seconds() 26 | assert 90750 == len(sbufPacket.SampleData) 27 | assert 1 == sbufPacket.NumSamples 28 | assert 1 == len(sbufPacket.SampleSizes) 29 | assert 90750 == sbufPacket.SampleSizes[0] 30 | assert 4 == len(sbufPacket.Attachments) 31 | assert 1 == len(sbufPacket.CreateIfNecessary) 32 | print(sbufPacket) 33 | 34 | 35 | def test_CMSampleBufferNoFdsc(): 36 | with open('./fixtures/asyn-feed-nofdsc', "rb") as f: 37 | data = f.read() 38 | sbufPacket = CMSampleBuffer.from_bytesVideo(data[16:]) 39 | assert False == sbufPacket.HasFormatDescription 40 | assert CMTimeConst.KCMTimeFlagsHasBeenRounded == sbufPacket.OutputPresentationTimestamp.CMTimeFlags 41 | assert 0x44b82fa09 == sbufPacket.OutputPresentationTimestamp.seconds() 42 | assert 1 == len(sbufPacket.SampleTimingInfoArray) 43 | assert 0 == sbufPacket.SampleTimingInfoArray[0].Duration.seconds() 44 | assert 0x44b82fa09 == sbufPacket.SampleTimingInfoArray[0].PresentationTimeStamp.seconds() 45 | assert 0 == sbufPacket.SampleTimingInfoArray[0].DecodeTimeStamp.seconds() 46 | assert 56604 == len(sbufPacket.SampleData) 47 | assert 1 == sbufPacket.NumSamples 48 | assert 1 == len(sbufPacket.SampleSizes) 49 | assert 56604 == sbufPacket.SampleSizes[0] 50 | assert 4 == len(sbufPacket.Attachments) 51 | assert 2 == len(sbufPacket.CreateIfNecessary) 52 | 53 | print(sbufPacket) 54 | 55 | 56 | def test_CMSampleBufferAudio(): 57 | with open('./fixtures/asyn-eat', "rb") as f: 58 | data = f.read() 59 | 60 | sbufPacket = CMSampleBuffer.from_bytesAudio(data[16:]) 61 | assert True == sbufPacket.HasFormatDescription 62 | assert 1024 == sbufPacket.NumSamples 63 | assert 1 == len(sbufPacket.SampleSizes) 64 | assert 4 == sbufPacket.SampleSizes[0] 65 | assert sbufPacket.NumSamples * sbufPacket.SampleSizes[0] == len(sbufPacket.SampleData) 66 | print(sbufPacket) 67 | 68 | 69 | def test_CMSampleBufferAudioNoFdsc(): 70 | with open('./fixtures/asyn-eat-nofdsc', "rb") as f: 71 | data = f.read() 72 | sbufPacket = CMSampleBuffer.from_bytesAudio(data[16:]) 73 | assert False == sbufPacket.HasFormatDescription 74 | assert 1024 == sbufPacket.NumSamples 75 | assert 1 == len(sbufPacket.SampleSizes) 76 | assert 4 == sbufPacket.SampleSizes[0] 77 | assert sbufPacket.NumSamples * sbufPacket.SampleSizes[0] == len(sbufPacket.SampleData) 78 | print(sbufPacket) 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /ioscreen/coremedia/consumer.py: -------------------------------------------------------------------------------- 1 | """ 2 | 最终流输出,需要继承 Consumer 类 3 | """ 4 | 5 | import io 6 | import logging 7 | import os 8 | import socket 9 | import struct 10 | 11 | from .CMFormatDescription import DescriptorConst 12 | from .CMSampleBuffer import CMSampleBuffer 13 | from .wav import set_wav_header 14 | 15 | startCode = b'\x00\x00\x00\x01' 16 | 17 | 18 | class Consumer: 19 | def consume(self, data: CMSampleBuffer): 20 | pass 21 | 22 | def stop(self): 23 | pass 24 | 25 | 26 | class AVFileWriter(Consumer): 27 | """ 保存 h264/wav 文件 28 | """ 29 | num = 0 30 | 31 | def __init__(self, h264FilePath=None, wavFilePath=None, outFilePath=None, audioOnly=False): 32 | self.h264FilePath = h264FilePath 33 | self.wavFilePath = wavFilePath 34 | self.h264FileWriter: io.open = io.open(h264FilePath, 'wb+') 35 | self.wavFileWriter: io.open = io.open(wavFilePath, 'wb+') 36 | self.outFilePath = outFilePath 37 | self.audioOnly = audioOnly 38 | 39 | def consume(self, data: CMSampleBuffer): 40 | if data.MediaType == DescriptorConst.MediaTypeSound: 41 | return self.consume_audio(data) 42 | if self.audioOnly: 43 | return 44 | return self.consume_video(data) 45 | 46 | def consume_video(self, data: CMSampleBuffer): 47 | if data.HasFormatDescription: 48 | self.write_h264(data.FormatDescription.PPS) 49 | self.write_h264(data.FormatDescription.SPS) 50 | if not data.SampleData: 51 | return True 52 | return self.write_h264s(data.SampleData) 53 | 54 | def consume_audio(self, data: CMSampleBuffer): 55 | if not data.SampleData: 56 | return True 57 | return self.wavFileWriter.write(data.SampleData) 58 | 59 | def write_h264s(self, buf): 60 | while len(buf) > 0: 61 | _length = struct.unpack('>I', buf[:4])[0] 62 | self.write_h264(buf[4:_length + 4]) 63 | buf = buf[_length + 4:] 64 | return True 65 | 66 | def write_h264(self, naluBuf): 67 | self.h264FileWriter.write(startCode) 68 | self.h264FileWriter.write(naluBuf) 69 | return True 70 | 71 | def stop(self): 72 | self.h264FileWriter.close() 73 | self.wavFileWriter.close() 74 | size = os.stat(self.wavFilePath).st_size 75 | with open(self.wavFilePath, 'rb+') as file: 76 | set_wav_header(size, file) 77 | 78 | 79 | class SocketUDP(Consumer): 80 | """ 81 | 发送 udp h264 裸流, 在 mac 有限制长度,需要 udp 长度切割,但是数据会积压延迟会变大,仅测试用 82 | :param naluBuf: 83 | :return: 84 | """ 85 | 86 | def __init__(self, broadcast=None, audioOnly=False): 87 | 88 | self.socket_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 89 | self.socket_udp.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 90 | self.broadcast = broadcast or ('127.0.0.1', 8880) 91 | self.audioOnly = audioOnly 92 | logging.info(f'listen UDP: udp/h264://{self.broadcast[0]}:{self.broadcast[1]}') 93 | 94 | def consume(self, data: CMSampleBuffer): 95 | if data.MediaType == DescriptorConst.MediaTypeSound: 96 | return 97 | return self.consume_video(data) 98 | 99 | def consume_video(self, data: CMSampleBuffer): 100 | if data.HasFormatDescription: 101 | self.write_udp(data.FormatDescription.PPS) 102 | self.write_udp(data.FormatDescription.SPS) 103 | if not data.SampleData: 104 | return True 105 | return self.write_buf(data.SampleData) 106 | 107 | def write_buf(self, buf): 108 | while len(buf) > 0: 109 | _length = struct.unpack('>I', buf[:4])[0] 110 | self.write_udp(buf[4:_length + 4]) 111 | buf = buf[_length + 4:] 112 | return True 113 | 114 | def write_udp(self, naluBuf): 115 | self.socket_udp.sendto(startCode, self.broadcast) 116 | index = 0 117 | ## vlc 接收长度过大会绿屏?? 118 | while len(naluBuf) >= index: 119 | self.socket_udp.sendto(naluBuf[index:index + 1024], self.broadcast) 120 | index += 1024 121 | return True 122 | 123 | def stop(self): 124 | self.socket_udp.close() 125 | 126 | -------------------------------------------------------------------------------- /ioscreen/coremedia/CMFormatDescription.py: -------------------------------------------------------------------------------- 1 | # iOS Frameworks 2 | # https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.9.sdk/System/Library/Frameworks/CoreMedia.framework/Versions/A/Headers/CMFormatDescription.h 3 | import enum 4 | import struct 5 | 6 | from .AudioStream import AudioStreamBasicDescription 7 | from .serialize import parse_length_magic, new_dictionary_from_bytes 8 | 9 | 10 | class DescriptorConst(enum.IntEnum): 11 | FormatDescriptorMagic = 0x66647363 # fdsc - csdf 12 | MediaTypeVideo = 0x76696465 # vide - ediv 13 | MediaTypeSound = 0x736F756E # nuos - soun 14 | MediaTypeMagic = 0x6D646961 # mdia - aidm 15 | VideoDimensionMagic = 0x7664696D # vdim - midv 16 | CodecMagic = 0x636F6463 # codc - cdoc 17 | CodecAvc1 = 0x61766331 # avc1 - 1cva 18 | ExtensionMagic = 0x6578746E # extn - ntxe 19 | AudioStreamBasicDescriptionMagic = 0x61736264 # asdb - dbsa 20 | 21 | 22 | class FormatDescriptor: 23 | 24 | def __init__(self, MediaType=None, VideoDimensionWidth=None, VideoDimensionHeight=None, Codec=None, Extensions=None, 25 | PPS=None, SPS=None, AudioStream=None): 26 | self.MediaType = MediaType 27 | self.VideoDimensionWidth = VideoDimensionWidth 28 | self.VideoDimensionHeight = VideoDimensionHeight 29 | self.Codec = Codec 30 | self.Extensions = Extensions 31 | self.PPS = PPS 32 | self.SPS = SPS 33 | self.AudioStreamBasicDescription: AudioStreamBasicDescription = AudioStream 34 | 35 | @classmethod 36 | def from_bytes(cls, buf): 37 | _, remainingBytes = parse_length_magic(buf, DescriptorConst.FormatDescriptorMagic) 38 | mediaType, remainingBytes = parse_media_type(remainingBytes) 39 | if mediaType == DescriptorConst.MediaTypeSound: 40 | length, _, = parse_length_magic(remainingBytes, DescriptorConst.AudioStreamBasicDescriptionMagic) 41 | AudioStream = AudioStreamBasicDescription.from_buffer_copy(remainingBytes[8:length]) 42 | return cls(MediaType=DescriptorConst.MediaTypeSound, AudioStream=AudioStream) 43 | else: 44 | videoDimensionWidth, videoDimensionHeight, remainingBytes = parse_video_dimension(remainingBytes) 45 | codec, remainingBytes = parse_codec(remainingBytes) 46 | Extensions = new_dictionary_from_bytes(remainingBytes, DescriptorConst.ExtensionMagic) 47 | pps, sps = extract_pps(Extensions) 48 | return cls(MediaType=DescriptorConst.MediaTypeVideo, Extensions=Extensions, PPS=pps, SPS=sps, Codec=codec, 49 | VideoDimensionHeight=videoDimensionHeight, VideoDimensionWidth=videoDimensionWidth) 50 | 51 | def __str__(self): 52 | if self.MediaType == DescriptorConst.MediaTypeVideo: 53 | return f'FormatDescriptor >>MediaType:{self.MediaType}, VideoDimension:({self.VideoDimensionWidth}x{self.VideoDimensionHeight}),' \ 54 | f'Codec:{self.Codec}, PPS:{self.PPS}, SPS:{self.SPS}, Extensions:{self.Extensions}' 55 | return f'FormatDescriptor >> MediaType:{self.MediaType}, AudioStreamBasicDescription: {self.AudioStreamBasicDescription} ' 56 | 57 | 58 | def parse_media_type(buf): 59 | length, _, = parse_length_magic(buf, DescriptorConst.MediaTypeMagic) 60 | mediaType = struct.unpack(' len(buf): 90 | raise Exception() 91 | if magic != exptectMagic: 92 | raise Exception() 93 | return int(_length), buf[8:] 94 | 95 | 96 | def parse_int_dict(buf): 97 | key, remainingBytes = parse_int_key(buf) 98 | value = parse_value(remainingBytes) 99 | return {key: value} 100 | 101 | 102 | def parse_int_key(buf): 103 | keyLength, _, = parse_length_magic(buf, DictConst.IntKey) 104 | key = struct.unpack('