├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── LICENSE ├── README.md ├── doc └── technical_documentation.md ├── docker ├── Dockerfile.debian ├── README.md └── build │ └── Dockerfile.debian ├── go.mod ├── go.sum ├── log ├── 513_direct_access_to_media_encoding_and_decoding.pdf ├── core_media_research.md ├── dtrace-probes.md ├── dump.wav ├── findPPS.md ├── ibridgecontrol.entitlements ├── linux-workarounds.md ├── need-analysis ├── out.wav ├── reply │ └── analysis.md └── usb-reverse.md ├── main.go └── screencapture ├── activator.go ├── common ├── nsnumber.go ├── nsnumber_test.go └── parserutil.go ├── coremedia ├── audio_stream_basic_description.go ├── audio_stream_basic_description_test.go ├── avfilewriter.go ├── avfilewriter_test.go ├── cmclock.go ├── cmclock_test.go ├── cmformatdescription.go ├── cmformatdescription_test.go ├── cmsamplebuf.go ├── cmsamplebuf_test.go ├── cmtime.go ├── cmtime_test.go ├── dict.go ├── dict_serializer.go ├── dict_serializer_test.go ├── dict_test.go ├── fixtures │ ├── adsb-from-fdsc │ ├── adsb-from-hpa-dict.bin │ ├── bulvalue.bin │ ├── complex_dict.bin │ ├── dict.bin │ ├── formatdescriptor-audio.bin │ ├── formatdescriptor.bin │ ├── intdict.bin │ ├── rply.bin │ └── serialize_dict.bin ├── nalutypetable.go ├── wav_format.go └── wav_format_test.go ├── diagnostics ├── consumer.go └── consumer_test.go ├── discovery.go ├── discovery_test.go ├── gstadapter ├── gst_adapter.go ├── gst_adapter_test.go ├── gst_pipeline_builder_linux.go └── gst_pipeline_builder_mac.go ├── interfaces.go ├── messageprocessor.go ├── messageprocessor_test.go ├── packet ├── asyn.go ├── asyn_feed.go ├── asyn_feed_test.go ├── asyn_rels.go ├── asyn_rels_test.go ├── asyn_sprp.go ├── asyn_sprp_test.go ├── asyn_srat.go ├── asyn_srat_test.go ├── asyn_tbas.go ├── asyn_tbas_test.go ├── asyn_test.go ├── asyn_tjmp.go ├── asyn_tjmp_test.go ├── fixtures │ ├── afmt-reply │ ├── afmt-request │ ├── asyn-eat │ ├── asyn-eat-nofdsc │ ├── asyn-feed │ ├── asyn-feed-nofdsc │ ├── asyn-feed-ttas-only │ ├── asyn-feed-unknown1 │ ├── asyn-hpa0 │ ├── asyn-hpa1 │ ├── asyn-hpd0 │ ├── asyn-hpd1 │ ├── asyn-need │ ├── asyn-rels │ ├── asyn-sprp │ ├── asyn-sprp2 │ ├── asyn-srat │ ├── asyn-tbas │ ├── asyn-tjmp │ ├── clok-reply │ ├── clok-request │ ├── cvrp-reply │ ├── cvrp-request │ ├── cwpa-reply1 │ ├── cwpa-reply2 │ ├── cwpa-request1 │ ├── cwpa-request2 │ ├── og-reply │ ├── og-request │ ├── skew-reply │ ├── skew-request │ ├── stop-reply │ ├── stop-request │ ├── time-reply1 │ └── time-request1 ├── ping.go ├── ping_test.go ├── sync.go ├── sync_afmt.go ├── sync_afmt_test.go ├── sync_clok.go ├── sync_clok_test.go ├── sync_cvrp.go ├── sync_cvrp_test.go ├── sync_cwpa.go ├── sync_cwpa_test.go ├── sync_og.go ├── sync_og_test.go ├── sync_skew.go ├── sync_skew_test.go ├── sync_stop.go ├── sync_stop_test.go ├── sync_time.go ├── sync_time_test.go ├── util.go └── util_test.go └── usbadapter.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [danielpaulus] 4 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | 3 | name: Unit tests 4 | jobs: 5 | test_on_mac: 6 | runs-on: macos-latest 7 | steps: 8 | - name: install gstreamer 9 | run: brew install libusb pkg-config gstreamer gst-plugins-bad gst-plugins-good gst-plugins-base gst-plugins-ugly 10 | - name: Install Go 11 | uses: actions/setup-go@v2 12 | with: 13 | go-version: 1.16.x 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: compile 17 | run: go build 18 | - name: run go test 19 | run: go test -v ./... 20 | test_on_linux: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Install Go 24 | uses: actions/setup-go@v2 25 | with: 26 | go-version: 1.16.x 27 | - name: Checkout code 28 | uses: actions/checkout@v2 29 | - name: update 30 | run: sudo apt-get update 31 | - name: install libusb 32 | run: sudo apt-get install -y libusb-1.0-0-dev 33 | - name: installlibglib 34 | run: sudo apt-get install -y libglib2.0-dev 35 | - name: install gstreamer 36 | run: sudo apt-get install -y libgstreamer1.0-0 libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio 37 | - name: compile 38 | run: go build 39 | - name: run go test 40 | run: go test -v ./... 41 | env: 42 | LINUX_CI: "true" 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | quicktime_video_hack 3 | *.wav 4 | *.ogg 5 | *.mp3 6 | # Binaries for programs and plugins 7 | main 8 | *.csv 9 | *.h264 10 | out.wav 11 | *.exe 12 | *.exe~ 13 | *.dll 14 | *.so 15 | *.dylib 16 | .idea 17 | # Test binary, built with `go test -c` 18 | *.test 19 | 20 | # Output of the go coverage tool, specifically when used with LiteIDE 21 | *.out 22 | 23 | # Dependency directories (remove the comment below to include it) 24 | # vendor/ 25 | 26 | .DS_Store 27 | *.[56789ao] 28 | *.a[56789o] 29 | *.pyc 30 | ._* 31 | .nfs.* 32 | [56789a].out 33 | *~ 34 | *.orig 35 | *.rej 36 | .*.swp 37 | core 38 | *.cgo*.go 39 | *.cgo*.c 40 | _cgo_* 41 | _obj 42 | _test 43 | _testmain.go 44 | 45 | /VERSION.cache 46 | /bin/ 47 | /build.out 48 | /doc/articles/wiki/*.bin 49 | /goinstall.log 50 | /last-change 51 | /misc/cgo/life/run.out 52 | /misc/cgo/stdio/run.out 53 | /misc/cgo/testso/main 54 | /pkg/ 55 | /src/*.*/ 56 | /src/cmd/cgo/zdefaultcc.go 57 | /src/cmd/dist/dist 58 | /src/cmd/go/internal/cfg/zdefaultcc.go 59 | /src/cmd/go/internal/cfg/zosarch.go 60 | /src/cmd/internal/objabi/zbootstrap.go 61 | /src/go/build/zcgo.go 62 | /src/go/doc/headscan 63 | /src/runtime/internal/sys/zversion.go 64 | /src/unicode/maketables 65 | /test.out 66 | /test/garbage/*.out 67 | /test/pass.out 68 | /test/run.out 69 | /test/times.out 70 | 71 | # This file includes artifacts of Go build that should not be checked in. 72 | # For files created by specific development environment (e.g. editor), 73 | # use alternative ways to exclude files from git. 74 | # For example, set up .git/info/exclude or use a global .gitignore. 75 | -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full-vnc 2 | 3 | USER gitpod 4 | 5 | # Install custom tools, runtime, etc. using apt-get 6 | # For example, the command below would install "bastet" - a command line tetris clone: 7 | # 8 | # RUN sudo apt-get -q update && # sudo apt-get install -yq bastet && # sudo rm -rf /var/lib/apt/lists/* 9 | # 10 | # More information: https://www.gitpod.io/docs/config-docker/ 11 | 12 | sudo apt-get install libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: go get && go build ./... && go test ./... 3 | command: go run 4 | image: 5 | file: .gitpod.Dockerfile 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 danielpaulus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://dcbadge.vercel.app/api/server/Zr8J3bCdkv)](https://discord.gg/Zr8J3bCdkv) 2 | [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/danielpaulus/quicktime_video_hack) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![CircleCI](https://circleci.com/gh/danielpaulus/quicktime_video_hack.svg?style=svg)](https://circleci.com/gh/danielpaulus/quicktime_video_hack) 5 | [![codecov](https://codecov.io/gh/danielpaulus/quicktime_video_hack/branch/master/graph/badge.svg)](https://codecov.io/gh/danielpaulus/quicktime_video_hack) 6 | [![Go Report](https://goreportcard.com/badge/github.com/danielpaulus/quicktime_video_hack)](https://goreportcard.com/report/github.com/danielpaulus/quicktime_video_hack) 7 | 8 | Release 0.6 9 | 10 | - qvh without Gstreamer is finally stable on MacOSX. I ran it for 16 hours straight on parallel devices and it worked flawlessly. 11 | - before a 1.0 Release I need to see if Gstreamer is stable enough and maybe fix or switch to ffmpeg 12 | - Linux support needs to be improved. It works but it is hard to get going currently. 13 | - Create an issue if you miss anything 14 | 15 | ## 1. What is this? 16 | 17 | This is an Operating System indepedent implementation for Quicktime Screensharing for iOS devices :-) 18 | 19 | [Check out my presentation](https://danielpaulus.github.io/quicktime_video_hack_presentation) 20 | 21 | [See me talk about it at GoWayFest](https://www.youtube.com/watch?v=jghi4nCBRwc) 22 | 23 | [See a demo on YouTube](https://youtu.be/8v5f_ybSjHk) 24 | 25 | This repository contains all the code you will need to grab and record video and audio from one or more iPhone(s) or iPad(s) 26 | without needing one of these expensive MacOS X computers or the hard to use QuickTime Player :-D 27 | 28 | - You can record video and audio as raw h264 and wave audio in the Apple demonstration mode (Device shows 9:41am, full battery and no cellphone carrier in the status bar) 29 | - Also you can just grab device audio as wave, ogg or mp3 without the Apple demonstration mode now 🎉 30 | - You can use custom Gstreamer Pipelines to transcode the AV data into whatever you like 31 | 32 | ## 2. Installation 33 | 34 | ### 2.1 Mac OSX 35 | 36 | 1. On MacOS run `brew install libusb pkg-config gstreamer gst-plugins-bad gst-plugins-good gst-plugins-base gst-plugins-ugly` 37 | 2. To just run: Download the latest release and run it 38 | 3. To develop: Clone the repo and execute `go run main.go` (need to install golang of course) 39 | 40 | ### 2.2 Linux 41 | 42 | 1. Run with Docker: the Docker files are [here](https://github.com/danielpaulus/quicktime_video_hack/tree/master/docker). There is one for just building and one for running. 43 | 44 | 2. If you want to build/run locally then copy paste the dependencies from this [Dockerfile](https://github.com/danielpaulus/quicktime_video_hack/blob/master/docker/Dockerfile.debian) and install with apt. 45 | 3. Git clone the repo and start hacking or download the latest release and run the binary :-D 46 | 47 | ## 3. Usage 48 | 49 | - For just displaying the screen run `qvh gstreamer` and it will work. 50 | - For just getting raw media output without Gstreamer involved use `qvh record out.h264 out.wav` or `qvh audio out.wav --wav` for audio only 51 | - For troubleshooting run `qvh diagnostics metrics.csv --dump=binary.bin` which will persist logs to a file, dump all usb transfers and gather metrics. 52 | - See `qvh gstreamer --examples` for transcoding media or streaming. 53 | - For creating mp3 or ogg in audio only mode see `qvh audio out.mp3 --mp3` and `qvh audio out.ogg --ogg` 54 | 55 | ## 4. Technical Docs/ Roll your own implementation 56 | 57 | QVH probably does something similar to what `QuickTime` and `com.apple.cmio.iOSScreenCaptureAssistant` are doing on MacOS. 58 | I have written some documentation here [doc/technical_documentation.md](https://github.com/danielpaulus/quicktime_video_hack/blob/master/doc/technical_documentation.md) 59 | So if you are just interested in the protocol or if you want to implement this in a different programming language than golang, read the docs. 60 | Also I have extracted binary dumps of all messages for writing unit tests and re-develop this in your preferred language in a test driven style. 61 | 62 | I have given up on windows support :-) 63 | ~~[Port to Windows](https://github.com/danielpaulus/quicktime_video_hack/tree/windows/windows) (I don't know why, but still people use Windows nowadays)~~ Did not find a way to do it 64 | -------------------------------------------------------------------------------- /docker/Dockerfile.debian: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | #Gstreamer dependencies take quite some time to install so I separate them 3 | RUN apt-get update && apt install -y libgstreamer1.0-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-doc gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio 4 | #Other dependencies 5 | RUN apt-get update && apt install -y wget zip libusb-1.0 6 | 7 | RUN wget https://github.com/danielpaulus/quicktime_video_hack/releases/download/v0.2-beta/bin.zip 8 | 9 | RUN unzip bin.zip 10 | RUN chmod +x /bin/linux/qvh 11 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Dockerfiles 2 | 3 | Here you can see how to get everything running easily. 4 | I have created separate Dockerfiles for building and for running. 5 | 6 | ### For building qvh use the Dockerfile in /build like so: 7 | 8 | #### 1. Build Image: 9 | 10 | - `docker build -f build/Dockerfile.debian -t "qvhbuild:$(git branch --show-current)" --build-arg GIT_BRANCH=$(git branch --show-current) .` 11 | 12 | #### 2. Get shell in container 13 | 14 | - `docker run -it qvhbuild:$(git branch --show-current) bash` 15 | 16 | ### For running qvh use the Dockerfile like so: 17 | 18 | #### 1. Build Image for Running: 19 | 20 | - `docker build -f Dockerfile.debian -t "qvhrun:$(git branch --show-current)" .` 21 | 22 | #### 2. Get shell in container 23 | 24 | - mount your host usb devices into the container and get a shell with the following command: 25 | - `docker run --privileged -v /dev/bus/usb:/dev/bus/usb -it qvhrun:$(git branch --show-current) bash` 26 | - use the `/bin/linux/qvh` binary to execute qvh 27 | -------------------------------------------------------------------------------- /docker/build/Dockerfile.debian: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | RUN apt-get update && apt install -y git wget libgstreamer-plugins-base1.0-dev libusb-1.0 3 | RUN wget https://golang.org/dl/go1.15.2.linux-amd64.tar.gz 4 | RUN tar -xvf go1.15.2.linux-amd64.tar.gz 5 | RUN mv go /usr/local 6 | ENV GOROOT=/usr/local/go 7 | ENV PATH=$GOPATH/bin:$GOROOT/bin:$PATH 8 | ARG GIT_BRANCH=master 9 | RUN git clone --depth 1 https://github.com/danielpaulus/quicktime_video_hack -b ${GIT_BRANCH} 10 | 11 | 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/danielpaulus/quicktime_video_hack 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/danielpaulus/go-ios v1.0.13 7 | github.com/danielpaulus/gst v0.0.0-20200201205042-e6d2974fceb8 8 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 9 | github.com/google/gousb v2.1.0+incompatible 10 | github.com/lijo-jose/glib v0.0.0-20191012030101-93ee72d7d646 11 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 12 | github.com/pkg/errors v0.9.1 13 | github.com/sirupsen/logrus v1.6.0 14 | github.com/stretchr/testify v1.6.1 15 | golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 // indirect 16 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 2 | github.com/danielpaulus/go-ios v1.0.12 h1:B0pirHZibKBanZgxZGrOwoejLlEb9zgtde7eHORxLVA= 3 | github.com/danielpaulus/go-ios v1.0.12/go.mod h1:k+X7QB2ffFOhTkly/nNwvV1VIysif1MWS1s8GgYXpU4= 4 | github.com/danielpaulus/go-ios v1.0.13 h1:MFc/QOcNXRY2vv/KRijES/sI0+aHTXfzgEeK6qsZOOU= 5 | github.com/danielpaulus/go-ios v1.0.13/go.mod h1:k+X7QB2ffFOhTkly/nNwvV1VIysif1MWS1s8GgYXpU4= 6 | github.com/danielpaulus/gst v0.0.0-20200201205042-e6d2974fceb8 h1:+XiTgRoo1bCA3paC4e/0WYWI7+J2O7hR/IYuSikANMw= 7 | github.com/danielpaulus/gst v0.0.0-20200201205042-e6d2974fceb8/go.mod h1:JbhjLST5AaUXpKQK65g9144BK8QHftbpuFoYuhDuONw= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= 12 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 13 | github.com/google/gousb v2.1.0+incompatible h1:ApzMDjF3FeO219QwWybJxYfFhXQzPLOEy0o+w9k5DNI= 14 | github.com/google/gousb v2.1.0+incompatible/go.mod h1:Tl4HdAs1ThE3gECkNwz+1MWicX6FXddhJEw7L8jRDiI= 15 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 16 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 18 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 19 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 20 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 21 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 22 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 23 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 24 | github.com/lijo-jose/glib v0.0.0-20191012030101-93ee72d7d646 h1:7I8sylThkL59rDoHMANuIxtB490DvrLAIZosNY9fDMM= 25 | github.com/lijo-jose/glib v0.0.0-20191012030101-93ee72d7d646/go.mod h1:wzypjnJX+g/LKnKDVvJni/u0gNlQestTwv6Kt/Qf3fk= 26 | github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= 27 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 28 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 29 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 30 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 34 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 35 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 36 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 37 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 38 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 39 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 41 | golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 h1:W0lCpv29Hv0UaM1LXb9QlBHLNP8UFfcKjblhVCWftOM= 42 | golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= 46 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 47 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 48 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 49 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 50 | howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5 h1:AQkaJpH+/FmqRjmXZPELom5zIERYZfwTjnHpfoVMQEc= 51 | howett.net/plist v0.0.0-20200419221736-3b63eb3a43b5/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= 52 | -------------------------------------------------------------------------------- /log/513_direct_access_to_media_encoding_and_decoding.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/log/513_direct_access_to_media_encoding_and_decoding.pdf -------------------------------------------------------------------------------- /log/core_media_research.md: -------------------------------------------------------------------------------- 1 | ### Transcript of WWDC talks 2 | https://asciiwwdc.com/2014/sessions/513 3 | 4 | ### CMTime 5 | http://developer.apple.com/library/ios/#documentation/CoreMedia/Reference/CMTime/Reference/reference.html 6 | 7 | https://stackoverflow.com/questions/3684883/coremedia-cmtime 8 | 9 | The Apple documentation contains information on the CMTime struct. 10 | 11 | As I understand it, you set the "TimeScale" to a timescale suitable for the media (e.g. 44100 = 1/44100 sec - which might be suitable for a CD). Then the "Value" represents units of that timescale. So, a value of 88200 would be 2 secs. 12 | ``` 13 | CMTime cmTime = new CMTime(); 14 | cmTime.TimeScale = 44100; 15 | cmTime.Value = 88200; 16 | ``` 17 | 18 | ### Xamarin C# native interface for CoreMedia 19 | 20 | https://github.com/xamarin/xamarin-macios/tree/master/src/CoreMedia 21 | 22 | ### Core Media Headers 23 | https://github.com/phracker/MacOSX-SDKs/tree/master/MacOSX10.9.sdk/System/Library/Frameworks/CoreMedia.framework 24 | 25 | ### what does packed mean 26 | https://developer.apple.com/library/archive/documentation/MusicAudio/Reference/CAFSpec/CAF_spec/CAF_spec.html#//apple_ref/doc/uid/TP40001862-CH210-BCGJEBBI -------------------------------------------------------------------------------- /log/dtrace-probes.md: -------------------------------------------------------------------------------- 1 | I had a look at a few USB API Calls: 2 | 3 | 4 | sudo dtrace -n '*:*:*IOUSBDevice*:entry { stack(); }' 5 | 6 | [iOS8 "com.apple.mobile.screenshotr" is replaced with the "com.apple.cmio.iOSScreenCaptureAssistant" service · Issue #122 · libimobiledevice/libimobiledevice · GitHub](https://github.com/libimobiledevice/libimobiledevice/issues/122) 7 | 8 | iOSScreenCaptureAssistant 9 | https://www.youtube.com/watch?v=A9gqzn3XcDM&feature=youtu.be 10 | 11 | /System/Library/Frameworks/CoreMediaIO.framework/Versions/A/Resources/iOSScreenCapture.plugin/Contents/Resources/iOSScreenCaptureAssistant 12 | 13 | 14 | 15 | 16 | sudo dtrace -n '*:*:*IOUSBDevice*:entry/execname=="iOSScreenCapture"/ { stack(); }' 17 | 18 | 19 | 20 | sudo dtrace -n '*:*:*IOUSBDevice*:entry/execname=="iOSScreenCapture"/ { printf("--%s--", execname); }' 21 | 22 | 23 | 24 | sudo dtrace -n '*:*IOUSBFamily*:*:entry/execname=="iOSScreenCapture"/ { printf("--%s--", execname); }' 25 | 26 | sudo dtrace -n '*:*:*ControlRequest*:entry/execname=="iOSScreenCapture"/ { printf("--%s--", probefunc); }' 27 | 28 | 29 | 30 | 31 | 32 | 33 | that is how you write data to usb: [objective c - USB device send/receive data - Stack Overflow](https://stackoverflow.com/questions/41038150/usb-device-send-receive-data) 34 | ```IOReturn WriteToDevice(IOUSBDeviceInterface **dev, UInt16 deviceAddress, 35 | UInt16 length, UInt8 writeBuffer[]) 36 | { 37 | 38 | IOUSBDevRequest request; 39 | request.bmRequestType = USBmakebmRequestType(kUSBOut, kUSBVendor, 40 | kUSBDevice); 41 | request.bRequest = 0xa0; 42 | request.wValue = deviceAddress; 43 | request.wIndex = 0; 44 | request.wLength = length; 45 | request.pData = writeBuffer; 46 | 47 | return (*dev)->DeviceRequest(dev, &request); 48 | } 49 | ``` 50 | so let's check out all the deviceRequests then 51 | 52 | 53 | sudo dtrace -n '*:*:*DeviceRequest*:entry/execname=="iOSScreenCapture"/ { tracemem(arg1, 8); }' 54 | sudo dtrace -n '*:*:*DeviceRequest*:entry { tracemem(arg1, 8); }' 55 | sudo dtrace -n '*:*:*DeviceRequest*:entry/execname=="iOSScreenCapture"/ { printf("devpointer:%#010x -- struct_ptr: %#010x", arg0, arg1); }' 56 | 57 | die methoden hier: 58 | [IOUSBFamily/IOUSBInterfaceUserClient.h at master · opensource-apple/IOUSBFamily · GitHub](https://github.com/opensource-apple/IOUSBFamily/blob/master/IOUSBUserClient/Headers/IOUSBInterfaceUserClient.h) 59 | 60 | 61 | who is setting the config? 62 | sudo dtrace -n '*:*:*SetConfiguration*:entry { printf("c:%s-b:%d", execname, arg1); ustack(); }' 63 | ``` 64 | 0 86572 _ZN21IOUSBDeviceUserClient16SetConfigurationEh:entry c:usbmuxd-b:6 65 | libsystem_kernel.dylib`mach_msg_trap+0xa 66 | IOKit`io_connect_method+0x176 67 | IOKit`IOConnectCallScalarMethod+0x4c 68 | IOUSBLib`IOUSBDeviceClass::SetConfiguration(unsigned char)+0x51 69 | usbmuxd`0x0000000104d6a02d+0x339 70 | IOKit`IODispatchCalloutFromCFMessage+0x164 71 | CoreFoundation`__CFMachPortPerform+0x11a 72 | CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__+0x29 73 | CoreFoundation`__CFRunLoopDoSource1+0x20f 74 | CoreFoundation`__CFRunLoopRun+0x9dc 75 | CoreFoundation`CFRunLoopRunSpecific+0x1c7 76 | CoreFoundation`CFRunLoopRun+0x28 77 | usbmuxd`0x0000000104d5d7b3+0x59e 78 | libdyld.dylib`start+0x1 79 | usbmuxd`0x2 80 | ``` 81 | 82 | seems like usbmuxd is doing it, so let's check it out:/System/Library/PrivateFrameworks/MobileDevice.framework/Versions/A/Resources/usbmuxd 83 | /Library/Preferences/com.apple.usbmuxd.plis 84 | interesting strings: AddNewInterface, FoundNewInterfaces 85 | sub_10000e02d 86 | 87 | 88 | ups: 89 | first 90 | ![a0b55237.png](:storage/49993860-1195-4bd7-9568-ea254440d571/a0b55237.png) 91 | and then 92 | ![1b82ea57.png](:storage/49993860-1195-4bd7-9568-ea254440d571/1b82ea57.png) 93 | -------------------------------------------------------------------------------- /log/dump.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/log/dump.wav -------------------------------------------------------------------------------- /log/findPPS.md: -------------------------------------------------------------------------------- 1 | run `ffmpeg -i video.mov -vcodec copy -vbsf h264_mp4toannexb -an outfile.h264` 2 | look for sps and pps in raw nalus, compare with fdsc extension and boom. 3 | 4 | --> raw pps nalu from h264 `27640033 AC568047 0133E69E 6E020202 04` 5 | --> fdsc extn `01640033 FFE10011 (27640033 AC568047 0133E69E 6E020202 04)010004 (28EE3CB0 FDF8F800)` 1. pps, 2. sps -------------------------------------------------------------------------------- /log/ibridgecontrol.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | com.apple.ibridge.control 8 | 9 | 10 | -------------------------------------------------------------------------------- /log/linux-workarounds.md: -------------------------------------------------------------------------------- 1 | 2 | THe usbmuxd will be killed via udev when the last device is removed. That is annoying in our case 3 | because we want to use the Listen command. Remove the following line in the udev conf: 4 | 5 | https://bbs.archlinux.org/viewtopic.php?id=229475 6 | ``` 7 | I just find a wordaround. The udev rule of usbmuxd (in /lib/udev/rules.d/39-usbmuxd.rules) is as following: 8 | 9 | ============================================================ 10 | # usbmuxd (Apple Mobile Device Muxer listening on /var/run/usbmuxd) 11 | 12 | # Initialize iOS devices into "deactivated" USB configuration state and activate usbmuxd 13 | ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="05ac", ATTR{idProduct}=="12[9a][0-9a-f]", ENV{USBMUX_SUPPORTED}="1", ATTR{bConfigurationValue}="0", OWNER="usbmux", TAG+="systemd", ENV{SYSTEMD_WANTS}="usbmuxd.service" 14 | 15 | # Exit usbmuxd when the last device is removed 16 | ACTION=="remove", SUBSYSTEM=="usb", ENV{PRODUCT}=="5ac/12[9a][0-9a-f]/*", ENV{INTERFACE}=="255/*", RUN+="/usr/bin/usbmuxd -x" 17 | ============================================================ 18 | 19 | 20 | It seems that when the last device is removed, the usbmuxd will exit automatically. So I comment out the last line of this file, then my phone can be recognized at everytime it is pluged in. 21 | ``` -------------------------------------------------------------------------------- /log/need-analysis: -------------------------------------------------------------------------------- 1 | NEED time diffs 2 | 25.348476 3 | 25.352846 ==> 4370 4 | 25.369731 ==> 16885 5 | 25.386373 ==> 16642 6 | 25.402885 ==> 16512 7 | 25.450929 ==> 48044 -------------------------------------------------------------------------------- /log/out.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/log/out.wav -------------------------------------------------------------------------------- /log/reply/analysis.md: -------------------------------------------------------------------------------- 1 | # Analysis of SYNC packets 2 | All SYNC packets require us to reply with a RPLY packet. 3 | It seems like this is used for synchronizing what would be a few CMClock's (implement CMSync.h protocol) 4 | on MacOSX. 5 | 6 | ## packet structure 7 | req: 8 | 4 byte length 9 | 4 byte sync marker 10 | 10(4-4-2) bytes 01000000 000000 0030 11 | 4 byte cwpa magic 12 | 8 byte correlation 13 | 6 bytes payload? 14 | 15 | resp: 16 | 4 byte length 17 | 4 byte reply magic 18 | 8 bytes correlation 19 | 2 bytes 0030 aus dem req 20 | 10 bytes payload? oder 4 bytes 0 und 6 bytes payload 21 | 22 | ## How they work 23 | It seems like the cwpa and cvrp sync packets are supposed to tell us to create CMClocks and send 24 | back a referenceID for those. 25 | I could observe that all subsequent Sync and Asyn Packets contain the same reference we send as a reply. 26 | So they probably tell us which CMClock to use for synching :-D 27 | 28 | ## Different Sync Packet Types 29 | 30 | This is an example list of packets received from the device in the exact order they appear 31 | in the hexdump and what i currently think they could mean 32 | 33 | |sync type |meaning | reply | | | 34 | |---|---|---|---|---| 35 | |cwpa |create clock, maybe for audio | contains a 6byte reference to the created clock | | | 36 | |afmt(lpcm) | probably audio format info | dict with error code 0 | | | 37 | |cvrp | create clock (maybe for video, the id is contained in all feed asyn packets) | contains a 6byte reference to the created clock | | | 38 | |clok | | | | | 39 | |time | | | | | 40 | |time | | | | | 41 | |3x skew | | | | | 42 | 43 | ## Details 44 | 45 | ### 1. CWPA Packet and Response 46 | 47 | #### Example Request 48 | 49 | | 4 Byte Length (36) |4 Byte Magic (SYNC) | 8 Empty clock reference| 2 bytes stuff(seems like a ID for sth.) | 4 byte message type (CWPA) | 8 byte correlation id | 6 bytes CFTypeID of the device clock | 50 | |---|---|---|---|---|---|---| 51 | |24000000 |636E7973 |01000000 00000000| 0030 | 61707763 |E03D5713 01000000| E074 5A130040 | 52 | 53 | #### Example Response 54 | 55 | Seems like the first two bytes of our clock identifier are always 0, and in the later packets appended so `0000 B00C E26CA67F` becomes `B00C E26CA67F 0000` 56 | | 4 Byte Length (28) |4 Byte Magic (RPLY) | 8 Byte correlation id | Seems like the ID from the req. + two 0 bytes | 8 bytes CFTypeID of our clock | 57 | |---|---|---|---|---|---| 58 | |1C000000 | 796C7072 |E03D5713 01000000 | 00300000 |0000 B00C E26CA67F | 59 | 60 | ### 2. AFMT Packet 61 | 62 | #### Example Request 63 | 64 | | 4 Byte Length (68) |4 Byte Magic (SYNC) | 8 bytes clock CFTypeID| 4 byte magic (AFMT)| 8 byte correlation id| 2x4byte some unknown data| 4 byte magic (LPCM) | 28 bytes what i think is pcm data| 65 | |---|---|---|---|---|---|---|---| 66 | |44000000| 636E7973| B00CE26C A67F0000| 746D6661 | 809D2213 01000000| 00000000 0070E740 |6D63706C| 4C000000 04000000 01000000 04000000 02000000 10000000 00000000| 67 | 68 | #### Example Response 69 | The response is basically a dictionary containing an error code, 0 if everything is ok :-D 70 | 71 | | 4 Byte Length (62) |4 Byte Magic (RPLY) | 8 correlation id| 4 byte 0| 4 byte dict length(42)| 4 byte magic (DICT)| dict bytes | 72 | |---|---|---|---|---|---|---| 73 | |3E000000 |796C7072| 809D2213 01000000 |00000000| 2A000000| 74636964| 22000000 7679656B 0D000000 6B727473 4572726F 720D0000 0076626D 6E030000 0000| 74 | 75 | ### 3. CVRP Packet 76 | 77 | #### Example Request 78 | 79 | Contains a Dict with a FormatDescription and timing information 80 | |4 Byte Length (649)|4 Byte Magic (SYNC)|8 byte empty(?) clock reference|4 byte magic(CVRP)|8 byte correlation id|CFTypeID of clock on device (needs to be in NEED packets we send)|4 byte length of dictionary (613)|4 byte magic (DICT)| Dict bytes| 81 | |---|---|---|---|---|---|---|---|---| 82 | |89020000 |636E7973| 01000000 00000000 |70727663| D0595613 01000000 |A08D5313 01000000 |65020000| 74636964| 0x.....| 83 | 84 | #### Example Response 85 | 86 | | 4 Byte Length (28) |4 Byte Magic (RPLY) | 8 Byte correlation id | 4 bytes (seem to be always 0) | 8 bytes CFTypeID of our clock(will be in all feed async packets) | 87 | |---|---|---|---|---| 88 | |1C000000 | 796C7072 |D0595613 01000000 | 00000000 |5002D16C A67F0000 | 89 | 90 | 91 | ### 4. CLOK Packet 92 | I am not quite sure what this is for, it seems like i am supposed to create a clock to then use it when sending two responses to time requests. 93 | Could be wrong though. 94 | 95 | #### Example Request 96 | 97 | | 4 Byte Length (28) |4 Byte Magic (SYNC) | 8 Byte clock CFTypeID | 4 bytes magic (CLOK) | 8 bytes correlation id | 98 | |---|---|---|---|---| 99 | |1C000000| 636E7973| 5002D16C A67F0000| 6B6F6C63 | 70495813 01000000 | 100 | 101 | #### Example Response 102 | 103 | | 4 Byte Length (28) |4 Byte Magic (RPLY) | 8 correlation id | 4 bytes (seem to be always 0) | 8 bytes CFTypeID of our clock(for the next two time packets) | 104 | |---|---|---|---|---| 105 | |1C000000| 796C7072| 70495813 01000000| 00000000 | 8079C17C A67F0000| 106 | 107 | ### 5. TIME Packet 108 | 109 | #### Example Request 110 | 111 | | 4 Byte Length (28) |4 Byte Magic (SYNC) | 8 Byte clock CFTypeID | 4 bytes magic (TIME) | 8 bytes correlation id | 112 | |---|---|---|---|---| 113 | |1C000000| 636E7973| 8079C17C A67F0000 |656D6974 | 503D2213 01000000 | 114 | 115 | 116 | 117 | #### Example Response 118 | 119 | | 4 Byte Length (44) |4 Byte Magic (RPLY) | 8 Byte correllation id | 4 bytes 0x0 | 24 bytes CMTime struct | 120 | |---|---|---|---|---| 121 | |2C000000 |796C7072 |503D2213 01000000| 00000000 | E1E142C4 62BA0000 00CA9A3B 01000000 00000000 00000000| 122 | 123 | ### 6. SKEW Packet 124 | 125 | ## Documentation links 126 | [`typedef unsigned long CFTypeID;`](https://developer.apple.com/documentation/corefoundation/cftypeid?language=objc) 127 | -------------------------------------------------------------------------------- /log/usb-reverse.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Notes: 4 | doing some of these things on mac will result in the kernel logging: 5 | "missing entitlement com.apple.ibridge.control" could this be it? 6 | Seems like there is no real way to do all this on a Mac, on the other hand, you have 7 | Quicktime there so why would you? :-D 8 | 9 | ## Steps 10 | 11 | run `main.go activate` to do the following stuff for you: 12 | On a Mac you can see QT is changing the device usb config when you open the video mirroring. 13 | That config however is usually missing on the ios device, to make it appear issue the following control transfer to the device: 14 | 0000 40 52 00 00 02 00 00 00 @R...... 15 | The device will then disconnect itself, and reconnect with an additional USB config. 16 | USBMUXD usually activates the new config automatically and now it is good to use with two additional bulk endpoints 17 | providing a video stream. 18 | 19 | 20 | 21 | 22 | ## random observations: 23 | 24 | What i can see from the ios qt 25 | 0000 00 01 20 00 12 00 00 00 00 00 00 00 00 00 00 00 .. ............. 26 | 0010 cd f9 6a 02 00 00 00 00 00 20 41 14 02 12 80 00 ..j...... A..... 27 | 0020 80 06 00 01 00 00 12 00 ........ 28 | 29 | 30 | 0000 00 01 20 01 12 00 00 00 00 00 00 00 00 00 00 00 .. ............. 31 | 0010 cd f9 6a 02 00 00 00 00 00 20 41 14 02 12 80 00 ..j...... A..... 32 | 0020 12 01 00 02 00 00 00 40 ac 05 a8 12 06 10 01 02 .......@........ 33 | 0030 03 05 .. 34 | 35 | last byte is bnumconfig 36 | 37 | 38 | libusb normal getActiveConfig Req 39 | 40 | 0000 00 01 20 00 01 00 00 00 00 00 00 00 00 00 00 00 .. ............. 41 | 0010 e5 b1 8e 02 00 00 00 00 00 20 41 14 02 17 80 00 ......... A..... 42 | 0020 80 08 00 00 00 00 01 00 ........ 43 | 44 | libusb normal getDeviceDescriptor Req 45 | 0000 00 01 20 00 01 00 00 00 00 00 00 00 00 00 00 00 .. ............. 46 | 0010 95 c6 8e 02 00 00 00 00 00 20 41 14 02 17 80 00 ......... A..... 47 | 0020 80 08 00 00 00 00 01 00 ........ 48 | 49 | doc on control transfers 50 | http://www.jungo.com/st/support/documentation/windriver/802/wdusb_man_mhtml/node55.html 51 | 52 | reading about clear feature https://www.beyondlogic.org/usbnutshell/usb6.shtml 53 | seems like clear feature, then 48 bytes ping can be reading 54 | 55 | first: 56 | Clear feature req: 57 | 0000 00 01 20 00 00 00 00 00 00 00 00 00 00 00 00 00 .. ............. 58 | 0010 d1 03 6b 02 00 00 00 00 00 20 41 14 02 12 00 00 ..k...... A..... 59 | 0020 02 01 00 00 86 00 00 00 ........ 60 | 61 | 0000 00 01 20 00 00 00 00 00 00 00 00 00 00 00 00 00 .. ............. 62 | 0010 a3 c0 98 02 00 00 00 00 00 00 20 14 02 0a 00 00 .......... ..... 63 | 0020 02 01 00 00 86 00 00 00 ........ 64 | 65 | 0000 00 01 20 00 00 00 00 00 00 00 00 00 00 00 00 00 .. ............. 66 | 0010 b7 fb 98 02 00 00 00 00 00 00 20 14 02 0a 00 00 .......... ..... 67 | 0020 02 01 00 00 86 00 00 00 ........ 68 | 69 | 70 | 71 | clear feature res: 72 | 0000 00 01 20 01 00 00 00 00 00 00 00 00 00 00 00 00 .. ............. 73 | 0010 d1 03 6b 02 00 00 00 00 00 20 41 14 02 12 00 00 ..k...... A..... 74 | 75 | 0000 00 01 20 01 00 00 00 00 00 00 00 00 00 00 00 00 .. ............. 76 | 0010 a3 c0 98 02 00 00 00 00 00 00 20 14 02 0a 00 00 .......... ..... 77 | 78 | 79 | second: 80 | clear feature req: 81 | 0000 00 01 20 00 00 00 00 00 00 00 00 00 00 00 00 00 .. ............. 82 | 0010 d3 03 6b 02 00 00 00 00 00 20 41 14 02 12 00 00 ..k...... A..... 83 | 0020 02 01 00 00 05 00 00 00 ........ 84 | 85 | 0000 00 01 20 00 00 00 00 00 00 00 00 00 00 00 00 00 .. ............. 86 | 0010 a5 c0 98 02 00 00 00 00 00 00 20 14 02 0a 00 00 .......... ..... 87 | 0020 02 01 00 00 05 00 00 00 ........ 88 | 89 | 90 | 91 | 92 | clear feature res: 93 | 0000 00 01 20 01 00 00 00 00 00 00 00 00 00 00 00 00 .. ............. 94 | 0010 d3 03 6b 02 00 00 00 00 00 20 41 14 02 12 00 00 ..k...... A..... 95 | 96 | 0000 00 01 20 01 00 00 00 00 00 00 00 00 00 00 00 00 .. ............. 97 | 0010 a5 c0 98 02 00 00 00 00 00 00 20 14 02 0a 00 00 .......... ..... 98 | 99 | 100 | 101 | 102 | 103 | then a ping is exchanged: 104 | read: 105 | 0020 10 00 00 00 67 6e 69 70 00 00 00 00 01 00 00 00 ....gnip........ 106 | 107 | 0020 10 00 00 00 67 6e 69 70 00 00 00 00 01 00 00 00 ....gnip........ 108 | 109 | write: 110 | 0020 10 00 00 00 67 6e 69 70 01 00 00 00 01 00 00 00 ....gnip........ 111 | 112 | 0020 10 00 00 00 67 6e 69 70 01 00 00 00 01 00 00 00 ....gnip........ 113 | 114 | -------------------------------------------------------------------------------- /screencapture/activator.go: -------------------------------------------------------------------------------- 1 | package screencapture 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/google/gousb" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // EnableQTConfig enables the hidden QuickTime Device configuration that will expose two new bulk endpoints. 12 | // We will send a control transfer to the device via USB which will cause the device to disconnect and then 13 | // re-connect with a new device configuration. Usually the usbmuxd will automatically enable that new config 14 | // as it will detect it as the device's preferredConfig. 15 | func EnableQTConfig(device IosDevice) (IosDevice, error) { 16 | usbSerial := device.SerialNumber 17 | ctx := gousb.NewContext() 18 | usbDevice, err := OpenDevice(ctx, device) 19 | if err != nil { 20 | return IosDevice{}, err 21 | } 22 | if isValidIosDeviceWithActiveQTConfig(usbDevice.Desc) { 23 | log.Debugf("Skipping %s because it already has an active QT config", usbSerial) 24 | return device, nil 25 | } 26 | 27 | sendQTConfigControlRequest(usbDevice) 28 | 29 | var i int 30 | for { 31 | log.Debugf("Checking for active QT config for %s", usbSerial) 32 | 33 | err = ctx.Close() 34 | if err != nil { 35 | log.Warn("failed closing context", err) 36 | } 37 | time.Sleep(500 * time.Millisecond) 38 | log.Debug("Reopening Context") 39 | ctx = gousb.NewContext() 40 | device, err = device.ReOpen(ctx) 41 | if err != nil { 42 | log.Debugf("device not found:%s", err) 43 | continue 44 | } 45 | i++ 46 | if i > 10 { 47 | log.Debug("Failed activating config") 48 | return IosDevice{}, fmt.Errorf("could not activate Quicktime Config for %s", usbSerial) 49 | } 50 | break 51 | } 52 | log.Debugf("QTConfig for %s activated", usbSerial) 53 | return device, err 54 | } 55 | 56 | func DisableQTConfig(device IosDevice) (IosDevice, error) { 57 | usbSerial := device.SerialNumber 58 | ctx := gousb.NewContext() 59 | usbDevice, err := OpenDevice(ctx, device) 60 | if err != nil { 61 | return IosDevice{}, err 62 | } 63 | if !isValidIosDeviceWithActiveQTConfig(usbDevice.Desc) { 64 | log.Debugf("Skipping %s because it is already deactivated", usbSerial) 65 | return device, nil 66 | } 67 | 68 | confignum, _ := usbDevice.ActiveConfigNum() 69 | log.Debugf("Config is active: %d, QT config is: %d", confignum, device.QTConfigIndex) 70 | 71 | for i := 0; i < 20; i++{ 72 | sendQTDisableConfigControlRequest(usbDevice) 73 | log.Debugf("Resetting device config (#%d)", i + 1) 74 | _, err := usbDevice.Config(device.UsbMuxConfigIndex) 75 | if err != nil { 76 | log.Warn(err) 77 | } 78 | } 79 | 80 | confignum, _ = usbDevice.ActiveConfigNum() 81 | log.Debugf("Config is active: %d, QT config is: %d", confignum, device.QTConfigIndex) 82 | 83 | 84 | return device, err 85 | } 86 | 87 | func sendQTConfigControlRequest(device *gousb.Device) { 88 | response := make([]byte, 0) 89 | val, err := device.Control(0x40, 0x52, 0x00, 0x02, response) 90 | if err != nil { 91 | log.Warnf("Failed sending control transfer for enabling hidden QT config. Seems like this happens sometimes but it still works usually: %s", err) 92 | } 93 | log.Debugf("Enabling QT config RC:%d", val) 94 | } 95 | 96 | func sendQTDisableConfigControlRequest(device *gousb.Device) { 97 | response := make([]byte, 0) 98 | val, err := device.Control(0x40, 0x52, 0x00, 0x00, response) 99 | 100 | if err != nil { 101 | log.Warnf("Failed sending control transfer for disabling hidden QT config:%s", err) 102 | 103 | } 104 | log.Debugf("Disabled QT config RC:%d", val) 105 | } 106 | -------------------------------------------------------------------------------- /screencapture/common/nsnumber.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "fmt" 7 | "log" 8 | "math" 9 | ) 10 | 11 | //NumberValueMagic is vbmn in little endian ascii ==> nmbv 12 | const NumberValueMagic uint32 = 0x6E6D6276 13 | 14 | // NSNumber represents a type in the binary protocol used. Type 6 seems to be a float64, type 4 a int64, type 3 a int32. 15 | // I am not sure whether signed or unsigned. They are all in LittleEndian 16 | type NSNumber struct { 17 | typeSpecifier byte 18 | //not certain if these are really unsigned 19 | IntValue uint32 20 | LongValue uint64 21 | FloatValue float64 22 | } 23 | 24 | //NewNSNumberFromUInt32 create NSNumber of type 0x03 with a 4 byte int as value 25 | func NewNSNumberFromUInt32(intValue uint32) NSNumber { 26 | return NSNumber{typeSpecifier: 03, IntValue: intValue} 27 | } 28 | 29 | //NewNSNumberFromUInt64 create NSNumber of type 0x04 with a 8 byte int as value 30 | func NewNSNumberFromUInt64(longValue uint64) NSNumber { 31 | return NSNumber{typeSpecifier: 04, LongValue: longValue} 32 | } 33 | 34 | //NewNSNumberFromUFloat64 create NSNumber of type 0x06 with a 8 byte int as value 35 | func NewNSNumberFromUFloat64(floatValue float64) NSNumber { 36 | return NSNumber{typeSpecifier: 06, FloatValue: floatValue} 37 | } 38 | 39 | //NewNSNumber reads a NSNumber from bytes. 40 | func NewNSNumber(bytes []byte) (NSNumber, error) { 41 | typeSpecifier := bytes[0] 42 | switch typeSpecifier { 43 | case 6: 44 | if len(bytes) != 9 { 45 | return NSNumber{}, fmt.Errorf("the NSNumber, type 6 should contain 8 bytes: %s", hex.Dump(bytes)) 46 | } 47 | value := math.Float64frombits(binary.LittleEndian.Uint64(bytes[1:])) 48 | return NSNumber{typeSpecifier: typeSpecifier, FloatValue: value}, nil 49 | case 5: 50 | if len(bytes) != 5 { 51 | return NSNumber{}, fmt.Errorf("the NSNumber, type 5 should contain 4 bytes: %s", hex.Dump(bytes)) 52 | } 53 | value := binary.LittleEndian.Uint32(bytes[1:]) 54 | return NSNumber{typeSpecifier: typeSpecifier, IntValue: value}, nil 55 | case 4: 56 | if len(bytes) != 9 { 57 | return NSNumber{}, fmt.Errorf("the NSNumber, type 4 should contain 8 bytes: %s", hex.Dump(bytes)) 58 | } 59 | value := binary.LittleEndian.Uint64(bytes[1:]) 60 | return NSNumber{typeSpecifier: typeSpecifier, LongValue: value}, nil 61 | case 3: 62 | if len(bytes) != 5 { 63 | return NSNumber{}, fmt.Errorf("the NSNumber, type 3 should contain 4 bytes: %s", hex.Dump(bytes)) 64 | } 65 | value := binary.LittleEndian.Uint32(bytes[1:]) 66 | return NSNumber{typeSpecifier: typeSpecifier, IntValue: value}, nil 67 | default: 68 | return NSNumber{}, fmt.Errorf("unknown NSNumber type %d", typeSpecifier) 69 | } 70 | 71 | } 72 | 73 | //ToBytes serializes a NSNumber into a []byte. 74 | //FIXME: remove allocation of array and use one that is passed in instead 75 | func (n NSNumber) ToBytes() []byte { 76 | switch n.typeSpecifier { 77 | case 6: 78 | result := make([]byte, 9) 79 | binary.LittleEndian.PutUint64(result[1:], math.Float64bits(n.FloatValue)) 80 | result[0] = n.typeSpecifier 81 | return result 82 | case 4: 83 | result := make([]byte, 9) 84 | binary.LittleEndian.PutUint64(result[1:], n.LongValue) 85 | result[0] = n.typeSpecifier 86 | return result 87 | case 3: 88 | result := make([]byte, 5) 89 | binary.LittleEndian.PutUint32(result[1:], n.IntValue) 90 | result[0] = n.typeSpecifier 91 | return result 92 | default: 93 | //shouldn't happen 94 | log.Fatalf("Unknown NSNumber type: %d", n.typeSpecifier) 95 | return nil 96 | } 97 | } 98 | 99 | func (n NSNumber) String() string { 100 | switch n.typeSpecifier { 101 | case 6: 102 | return fmt.Sprintf("Float64[%f]", n.FloatValue) 103 | case 3: 104 | return fmt.Sprintf("Int32[%d]", n.IntValue) 105 | case 4: 106 | return fmt.Sprintf("UInt64[%d]", n.LongValue) 107 | default: 108 | return "Invalid Type Specifier" 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /screencapture/common/nsnumber_test.go: -------------------------------------------------------------------------------- 1 | package common_test 2 | 3 | import ( 4 | "github.com/danielpaulus/quicktime_video_hack/screencapture/common" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | //I took these from hexdumps 10 | var typeSix = []byte{0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9E, 0x40} 11 | var typeFive = []byte{0x05, 0x2E, 00, 00, 00} 12 | var typeFour = []byte{0x04, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} 13 | var typeThree = []byte{0x03, 0x1E, 00, 00, 00} 14 | 15 | const typeSixDecoded float64 = 1920 16 | const typeFiveDecoded uint32 = 46 17 | const typeFourDecoded uint64 = 5 18 | const typeThreeDecoded uint32 = 30 19 | 20 | func TestErrors(t *testing.T) { 21 | var broken []byte 22 | broken = make([]byte, len(typeSix)) 23 | copy(broken, typeSix) 24 | broken[0] = 3 25 | _, err := common.NewNSNumber(broken) 26 | assert.Error(t, err) 27 | 28 | broken = make([]byte, len(typeThree)) 29 | copy(broken, typeThree) 30 | broken[0] = 6 31 | _, err = common.NewNSNumber(broken) 32 | assert.Error(t, err) 33 | 34 | broken[0] = 4 35 | _, err = common.NewNSNumber(broken) 36 | assert.Error(t, err) 37 | 38 | broken[0] = 56 39 | _, err = common.NewNSNumber(broken) 40 | assert.Error(t, err) 41 | 42 | broken = make([]byte, len(typeFive)) 43 | copy(broken, typeFive) 44 | broken[0] = 134 45 | _, err = common.NewNSNumber(broken) 46 | assert.Error(t, err) 47 | } 48 | 49 | func TestNumberValue(t *testing.T) { 50 | 51 | float64Num, err := common.NewNSNumber(typeSix) 52 | if assert.NoError(t, err) { 53 | assert.Equal(t, typeSixDecoded, float64Num.FloatValue) 54 | } 55 | 56 | uint32Num, err := common.NewNSNumber(typeFive) 57 | if assert.NoError(t, err) { 58 | assert.Equal(t, typeFiveDecoded, uint32Num.IntValue) 59 | } 60 | 61 | uint64Num, err := common.NewNSNumber(typeFour) 62 | if assert.NoError(t, err) { 63 | assert.Equal(t, typeFourDecoded, uint64Num.LongValue) 64 | } 65 | 66 | uint32Num, err = common.NewNSNumber(typeThree) 67 | if assert.NoError(t, err) { 68 | assert.Equal(t, typeThreeDecoded, uint32Num.IntValue) 69 | } 70 | } 71 | 72 | func TestEncoding(t *testing.T) { 73 | floatNSNumber := common.NewNSNumberFromUFloat64(typeSixDecoded) 74 | assert.Equal(t, typeSix, floatNSNumber.ToBytes()) 75 | 76 | int32NSNumber := common.NewNSNumberFromUInt32(typeThreeDecoded) 77 | assert.Equal(t, typeThree, int32NSNumber.ToBytes()) 78 | 79 | int64NSNumber := common.NewNSNumberFromUInt64(typeFourDecoded) 80 | assert.Equal(t, typeFour, int64NSNumber.ToBytes()) 81 | } 82 | -------------------------------------------------------------------------------- /screencapture/common/parserutil.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "fmt" 7 | ) 8 | 9 | //WriteLengthAndMagic just writes length and magic as uint32 4 byte values into the given array. 10 | func WriteLengthAndMagic(bytes []byte, length int, magic uint32) { 11 | binary.LittleEndian.PutUint32(bytes, uint32(length)) 12 | binary.LittleEndian.PutUint32(bytes[4:], magic) 13 | } 14 | 15 | //ParseLengthAndMagic checks if if the given byte array is longer or equal the uint32 in the first 4 bytes, and if the magic value in the second 4 bytes equals the supplied magic 16 | // and returns the length, a slice of the bytes without length and magic or an error. 17 | func ParseLengthAndMagic(bytes []byte, exptectedMagic uint32) (int, []byte, error) { 18 | length := binary.LittleEndian.Uint32(bytes) 19 | magic := binary.LittleEndian.Uint32(bytes[4:]) 20 | if int(length) > len(bytes) { 21 | return 0, bytes, fmt.Errorf("invalid length in header: %d but only received: %d bytes", length, len(bytes)) 22 | } 23 | if magic != exptectedMagic { 24 | unknownMagic := string(bytes[4:8]) 25 | return 0, nil, fmt.Errorf("unknown magic type:%s (0x%x), cannot parse value %s", unknownMagic, magic, hex.Dump(bytes)) 26 | } 27 | return int(length), bytes[8:], nil 28 | } 29 | -------------------------------------------------------------------------------- /screencapture/coremedia/audio_stream_basic_description.go: -------------------------------------------------------------------------------- 1 | package coremedia 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "math" 8 | ) 9 | 10 | //AudioFormatIDLpcm is the CoreMedia MediaID for LPCM 11 | const AudioFormatIDLpcm uint32 = 0x6C70636D 12 | 13 | //AudioStreamBasicDescription represents the struct found here: https://github.com/nu774/MSResampler/blob/master/CoreAudio/CoreAudioTypes.h 14 | type AudioStreamBasicDescription struct { 15 | SampleRate float64 16 | FormatID uint32 17 | FormatFlags uint32 18 | BytesPerPacket uint32 19 | FramesPerPacket uint32 20 | BytesPerFrame uint32 21 | ChannelsPerFrame uint32 22 | BitsPerChannel uint32 23 | Reserved uint32 24 | } 25 | 26 | //DefaultAudioStreamBasicDescription creates a LPCM AudioStreamBasicDescription with default values I grabbed from the hex dump 27 | func DefaultAudioStreamBasicDescription() AudioStreamBasicDescription { 28 | return AudioStreamBasicDescription{FormatFlags: 12, 29 | BytesPerPacket: 4, FramesPerPacket: 1, BytesPerFrame: 4, ChannelsPerFrame: 2, BitsPerChannel: 16, Reserved: 0, 30 | SampleRate: 48000, FormatID: AudioFormatIDLpcm} 31 | } 32 | 33 | func (adsb AudioStreamBasicDescription) String() string { 34 | return fmt.Sprintf("{SampleRate:%f,FormatFlags:%d,BytesPerPacket:%d,FramesPerPacket:%d,BytesPerFrame:%d,ChannelsPerFrame:%d,BitsPerChannel:%d,Reserved:%d}", 35 | adsb.SampleRate, adsb.FormatFlags, adsb.BytesPerPacket, adsb.FramesPerPacket, 36 | adsb.BytesPerFrame, adsb.ChannelsPerFrame, adsb.BitsPerChannel, adsb.Reserved) 37 | } 38 | 39 | //NewAudioStreamBasicDescriptionFromBytes reads AudioStreamBasicDescription from bytes 40 | func NewAudioStreamBasicDescriptionFromBytes(data []byte) (AudioStreamBasicDescription, error) { 41 | r := bytes.NewReader(data) 42 | var audioStreamBasicDescription AudioStreamBasicDescription 43 | err := binary.Read(r, binary.LittleEndian, &audioStreamBasicDescription) 44 | if err != nil { 45 | return audioStreamBasicDescription, err 46 | } 47 | return audioStreamBasicDescription, nil 48 | } 49 | 50 | //SerializeAudioStreamBasicDescription puts an AudioStreamBasicDescription into the given byte array 51 | func (adsb AudioStreamBasicDescription) SerializeAudioStreamBasicDescription(adsbBytes []byte) { 52 | binary.LittleEndian.PutUint64(adsbBytes, math.Float64bits(adsb.SampleRate)) 53 | var index = 8 54 | binary.LittleEndian.PutUint32(adsbBytes[index:], AudioFormatIDLpcm) 55 | index += 4 56 | 57 | binary.LittleEndian.PutUint32(adsbBytes[index:], adsb.FormatFlags) 58 | index += 4 59 | binary.LittleEndian.PutUint32(adsbBytes[index:], adsb.BytesPerPacket) 60 | index += 4 61 | binary.LittleEndian.PutUint32(adsbBytes[index:], adsb.FramesPerPacket) 62 | index += 4 63 | binary.LittleEndian.PutUint32(adsbBytes[index:], adsb.BytesPerFrame) 64 | index += 4 65 | binary.LittleEndian.PutUint32(adsbBytes[index:], adsb.ChannelsPerFrame) 66 | index += 4 67 | binary.LittleEndian.PutUint32(adsbBytes[index:], adsb.BitsPerChannel) 68 | index += 4 69 | binary.LittleEndian.PutUint32(adsbBytes[index:], adsb.Reserved) 70 | index += 4 71 | 72 | binary.LittleEndian.PutUint64(adsbBytes[index:], math.Float64bits(adsb.SampleRate)) 73 | index += 8 74 | binary.LittleEndian.PutUint64(adsbBytes[index:], math.Float64bits(adsb.SampleRate)) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /screencapture/coremedia/audio_stream_basic_description_test.go: -------------------------------------------------------------------------------- 1 | package coremedia 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestAudioStreamBasicDescriptionSerializer(t *testing.T) { 12 | dat, err := ioutil.ReadFile("fixtures/adsb-from-hpa-dict.bin") 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | buffer := make([]byte, 56) 17 | adsb := AudioStreamBasicDescription{FormatFlags: 12, 18 | BytesPerPacket: 4, FramesPerPacket: 1, BytesPerFrame: 4, ChannelsPerFrame: 2, BitsPerChannel: 16, Reserved: 0, 19 | SampleRate: 48000} 20 | adsb.SerializeAudioStreamBasicDescription(buffer) 21 | 22 | assert.Equal(t, dat, buffer) 23 | 24 | parsedAdsb, err := NewAudioStreamBasicDescriptionFromBytes(buffer) 25 | assert.Equal(t, adsb.String(), parsedAdsb.String()) 26 | } 27 | -------------------------------------------------------------------------------- /screencapture/coremedia/avfilewriter.go: -------------------------------------------------------------------------------- 1 | package coremedia 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | ) 7 | 8 | var startCode = []byte{00, 00, 00, 01} 9 | 10 | //AVFileWriter writes nalus into a file using 0x00000001 as a separator (h264 ANNEX B) and raw pcm audio into a wav file 11 | //Note that you will have to call WriteWavHeader() on the audiofile when you are done to write a wav header and get a valid file. 12 | type AVFileWriter struct { 13 | h264FileWriter io.Writer 14 | wavFileWriter io.Writer 15 | outFilePath string 16 | audioOnly bool 17 | } 18 | 19 | //NewAVFileWriter binary writes nalus in annex b format to the given writer and audio buffers into a wav file. 20 | //Note that you will have to call WriteWavHeader() on the audiofile when you are done to write a wav header and get a valid file. 21 | func NewAVFileWriter(h264FileWriter io.Writer, wavFileWriter io.Writer) AVFileWriter { 22 | return AVFileWriter{h264FileWriter: h264FileWriter, wavFileWriter: wavFileWriter, audioOnly: false} 23 | } 24 | 25 | func NewAVFileWriterAudioOnly(wavFileWriter io.Writer) AVFileWriter { 26 | return AVFileWriter{h264FileWriter: nil, wavFileWriter: wavFileWriter, audioOnly: true} 27 | } 28 | 29 | //Consume writes PPS and SPS as well as sample bufs into a annex b .h264 file and audio samples into a wav file 30 | //Note that you will have to call WriteWavHeader() on the audiofile when you are done to write a wav header and get a valid file. 31 | func (avfw AVFileWriter) Consume(buf CMSampleBuffer) error { 32 | if buf.MediaType == MediaTypeSound { 33 | return avfw.consumeAudio(buf) 34 | } 35 | if avfw.audioOnly { 36 | return nil 37 | } 38 | return avfw.consumeVideo(buf) 39 | } 40 | 41 | //Nothing currently 42 | func (avfw AVFileWriter) Stop() {} 43 | 44 | func (avfw AVFileWriter) consumeVideo(buf CMSampleBuffer) error { 45 | if buf.HasFormatDescription { 46 | err := avfw.writeNalu(buf.FormatDescription.PPS) 47 | if err != nil { 48 | return err 49 | } 50 | err = avfw.writeNalu(buf.FormatDescription.SPS) 51 | if err != nil { 52 | return err 53 | } 54 | } 55 | if !buf.HasSampleData() { 56 | return nil 57 | } 58 | return avfw.writeNalus(buf.SampleData) 59 | } 60 | 61 | func (avfw AVFileWriter) writeNalus(bytes []byte) error { 62 | slice := bytes 63 | for len(slice) > 0 { 64 | length := binary.BigEndian.Uint32(slice) 65 | err := avfw.writeNalu(slice[4 : length+4]) 66 | if err != nil { 67 | return err 68 | } 69 | slice = slice[length+4:] 70 | } 71 | return nil 72 | } 73 | 74 | func (avfw AVFileWriter) writeNalu(naluBytes []byte) error { 75 | _, err := avfw.h264FileWriter.Write(startCode) 76 | if err != nil { 77 | return err 78 | } 79 | _, err = avfw.h264FileWriter.Write(naluBytes) 80 | if err != nil { 81 | return err 82 | } 83 | return nil 84 | } 85 | 86 | func (avfw AVFileWriter) consumeAudio(buffer CMSampleBuffer) error { 87 | if !buffer.HasSampleData() { 88 | return nil 89 | } 90 | _, err := avfw.wavFileWriter.Write(buffer.SampleData) 91 | if err != nil { 92 | return err 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /screencapture/coremedia/avfilewriter_test.go: -------------------------------------------------------------------------------- 1 | package coremedia_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "testing" 8 | 9 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | var fakePPS = []byte{1, 2, 3, 4, 5} 14 | var fakeSPS = []byte{6, 7, 8, 9} 15 | var startCode = []byte{00, 00, 00, 01} 16 | 17 | func TestFileWriter(t *testing.T) { 18 | buf := bytes.NewBuffer(make([]byte, 100)) 19 | buf.Reset() 20 | avfw := coremedia.NewAVFileWriter(buf, nil) 21 | err := avfw.Consume(cmSampleBufWithAFewBytes()) 22 | assert.NoError(t, err) 23 | assert.Equal(t, 6, buf.Len()) 24 | assert.Equal(t, []byte{00, 00, 00, 01, 00, 00}, buf.Bytes()) 25 | buf.Reset() 26 | err = avfw.Consume(cmSampleBufWithFdscAndAFewBytes()) 27 | assert.NoError(t, err) 28 | expectedBytes := append(startCode, fakePPS...) 29 | expectedBytes = append(expectedBytes, startCode...) 30 | expectedBytes = append(expectedBytes, fakeSPS...) 31 | expectedBytes = append(expectedBytes, []byte{00, 00, 00, 01, 00, 00}...) 32 | assert.Equal(t, expectedBytes, buf.Bytes()) 33 | 34 | avfw = coremedia.NewAVFileWriter(failingWriter{}, nil) 35 | err = avfw.Consume(cmSampleBufWithFdscAndAFewBytes()) 36 | assert.Error(t, err) 37 | err = avfw.Consume(cmSampleBufWithAFewBytes()) 38 | assert.Error(t, err) 39 | } 40 | 41 | func TestFileWriterForAudio(t *testing.T) { 42 | buf := bytes.NewBuffer(make([]byte, 100)) 43 | buf.Reset() 44 | avfw := coremedia.NewAVFileWriter(nil, buf) 45 | sampleBuffer := cmSampleBufWithFdscAndAFewBytes() 46 | sampleBuffer.MediaType = coremedia.MediaTypeSound 47 | err := avfw.Consume(sampleBuffer) 48 | if assert.NoError(t, err) { 49 | assert.Equal(t, sampleBuffer.SampleData, buf.Bytes()) 50 | } 51 | 52 | avfw = coremedia.NewAVFileWriter(nil, failingWriter{}) 53 | err = avfw.Consume(sampleBuffer) 54 | assert.Error(t, err) 55 | } 56 | 57 | type failingWriter struct{} 58 | 59 | func (f failingWriter) Write(p []byte) (n int, err error) { 60 | return 0, errors.New("failed") 61 | } 62 | 63 | func cmSampleBufWithFdscAndAFewBytes() coremedia.CMSampleBuffer { 64 | fakeNalu := make([]byte, 6) 65 | binary.BigEndian.PutUint32(fakeNalu, 2) 66 | return cmSampleBufWithFdscAndSampleData(fakeNalu) 67 | } 68 | 69 | func cmSampleBufWithFdscAndSampleData(sampleData []byte) coremedia.CMSampleBuffer { 70 | return coremedia.CMSampleBuffer{ 71 | OutputPresentationTimestamp: coremedia.CMTime{}, 72 | FormatDescription: coremedia.FormatDescriptor{PPS: fakePPS, SPS: fakeSPS}, 73 | HasFormatDescription: true, 74 | NumSamples: 0, 75 | SampleTimingInfoArray: nil, 76 | SampleData: sampleData, 77 | SampleSizes: nil, 78 | Attachments: coremedia.IndexKeyDict{}, 79 | Sary: coremedia.IndexKeyDict{}, 80 | } 81 | } 82 | 83 | func cmSampleBufWithAFewBytes() coremedia.CMSampleBuffer { 84 | fakeNalu := make([]byte, 6) 85 | binary.BigEndian.PutUint32(fakeNalu, 2) 86 | return cmSampleBufWithSampleData(fakeNalu) 87 | } 88 | func cmSampleBufWithSampleData(sampleData []byte) coremedia.CMSampleBuffer { 89 | return coremedia.CMSampleBuffer{ 90 | OutputPresentationTimestamp: coremedia.CMTime{}, 91 | FormatDescription: coremedia.FormatDescriptor{}, 92 | HasFormatDescription: false, 93 | NumSamples: 0, 94 | SampleTimingInfoArray: nil, 95 | SampleData: sampleData, 96 | SampleSizes: nil, 97 | Attachments: coremedia.IndexKeyDict{}, 98 | Sary: coremedia.IndexKeyDict{}, 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /screencapture/coremedia/cmclock.go: -------------------------------------------------------------------------------- 1 | package coremedia 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // CMClock represents a monotonic Clock that will start counting when created 8 | type CMClock struct { 9 | ID uint64 10 | TimeScale uint32 11 | factor float64 12 | startTime time.Time 13 | } 14 | 15 | //NanoSecondScale is the default system clock scale where 1/NanoSecondScale == 1 Nanosecond. 16 | const NanoSecondScale = 1000000000 17 | 18 | //NewCMClockWithHostTime creates a new Clock with the given ID with a nanosecond scale. 19 | //Calls to GetTime will measure the time difference since the clock was created. 20 | func NewCMClockWithHostTime(ID uint64) CMClock { 21 | return CMClock{ 22 | ID: ID, 23 | TimeScale: NanoSecondScale, 24 | factor: 1, 25 | startTime: time.Now(), 26 | } 27 | } 28 | 29 | //NewCMClockWithHostTimeAndScale creates a new CMClock with given ID and a custom timeScale 30 | func NewCMClockWithHostTimeAndScale(ID uint64, timeScale uint32) CMClock { 31 | return CMClock{ 32 | ID: ID, 33 | TimeScale: timeScale, 34 | factor: float64(timeScale) / float64(NanoSecondScale), 35 | startTime: time.Now(), 36 | } 37 | } 38 | 39 | //GetTime returns a CMTime that gives the time passed since the clock started. 40 | //This is monotonic and does NOT use wallclock time. 41 | func (c CMClock) GetTime() CMTime { 42 | return CMTime{ 43 | CMTimeValue: c.calcValue(time.Since(c.startTime).Nanoseconds()), 44 | CMTimeScale: c.TimeScale, 45 | CMTimeFlags: KCMTimeFlagsHasBeenRounded, 46 | CMTimeEpoch: 0, 47 | } 48 | } 49 | 50 | func (c CMClock) calcValue(val int64) uint64 { 51 | if NanoSecondScale == c.TimeScale { 52 | return uint64(val) 53 | } 54 | return uint64(c.factor * float64(val)) 55 | } 56 | 57 | //CalculateSkew calculates the deviation between the frequencies of two given clocks by using time diffs and returns a skew value float64 58 | //scaled to match the second clock. 59 | func CalculateSkew(startTimeClock1 CMTime, endTimeClock1 CMTime, startTimeClock2 CMTime, endTimeClock2 CMTime) float64 { 60 | timeDiffClock1 := endTimeClock1.CMTimeValue - startTimeClock1.CMTimeValue 61 | timeDiffClock2 := endTimeClock2.CMTimeValue - startTimeClock2.CMTimeValue 62 | 63 | diffTime := CMTime{CMTimeValue: timeDiffClock1, CMTimeScale: startTimeClock1.CMTimeScale} 64 | scaledDiff := diffTime.GetTimeForScale(startTimeClock2) 65 | //println("scaleddiff:" + scaledDiff) 66 | return float64(startTimeClock2.CMTimeScale) * scaledDiff / float64(timeDiffClock2) 67 | } 68 | -------------------------------------------------------------------------------- /screencapture/coremedia/cmclock_test.go: -------------------------------------------------------------------------------- 1 | package coremedia_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCMClock_GetTime(t *testing.T) { 11 | cmclock := coremedia.NewCMClockWithHostTime(uint64(5)) 12 | assert.Equal(t, uint32(coremedia.NanoSecondScale), cmclock.TimeScale) 13 | time1 := cmclock.GetTime() 14 | time2 := cmclock.GetTime() 15 | assert.Equal(t, cmclock.TimeScale, time1.CMTimeScale) 16 | //The clock is monotonic with nanosecond precision, so this should always be true 17 | assert.True(t, true, time2.CMTimeValue > time1.CMTimeValue) 18 | 19 | cmclock = coremedia.NewCMClockWithHostTimeAndScale(0, 1) 20 | assert.Equal(t, uint64(0), cmclock.GetTime().CMTimeValue) 21 | } 22 | 23 | func TestCalculateSkew(t *testing.T) { 24 | testCases := map[string]struct { 25 | startTimeClock1 coremedia.CMTime 26 | endTimeClock1 coremedia.CMTime 27 | startTimeClock2 coremedia.CMTime 28 | endTimeClock2 coremedia.CMTime 29 | expectedValue float64 30 | }{ 31 | "check simple case, no skew": { 32 | coremedia.CMTime{CMTimeValue: 0, CMTimeScale: 48000}, 33 | coremedia.CMTime{CMTimeValue: 1, CMTimeScale: 48000}, 34 | coremedia.CMTime{CMTimeValue: 0, CMTimeScale: 48000}, 35 | coremedia.CMTime{CMTimeValue: 1, CMTimeScale: 48000}, 36 | float64(48000.0)}, 37 | "check simple case, positive skew": { 38 | coremedia.CMTime{CMTimeValue: 0, CMTimeScale: 48000}, 39 | coremedia.CMTime{CMTimeValue: 2, CMTimeScale: 48000}, 40 | coremedia.CMTime{CMTimeValue: 0, CMTimeScale: 48000}, 41 | coremedia.CMTime{CMTimeValue: 1, CMTimeScale: 48000}, 42 | float64(96000.0)}, 43 | "check simple case, negative skew": { 44 | coremedia.CMTime{CMTimeValue: 0, CMTimeScale: 48000}, 45 | coremedia.CMTime{CMTimeValue: 2000, CMTimeScale: 48000}, 46 | coremedia.CMTime{CMTimeValue: 0, CMTimeScale: 48000}, 47 | coremedia.CMTime{CMTimeValue: 2001, CMTimeScale: 48000}, 48 | float64(47976.011994003)}, 49 | "check different scales, negative skew": { 50 | coremedia.CMTime{CMTimeValue: 0, CMTimeScale: coremedia.NanoSecondScale}, 51 | coremedia.CMTime{CMTimeValue: 20833 * 5, CMTimeScale: coremedia.NanoSecondScale}, 52 | coremedia.CMTime{CMTimeValue: 0, CMTimeScale: 48000}, 53 | coremedia.CMTime{CMTimeValue: 5, CMTimeScale: 48000}, 54 | float64(47999.232)}, 55 | "check different scales, positive skew": { 56 | coremedia.CMTime{CMTimeValue: 0, CMTimeScale: coremedia.NanoSecondScale}, 57 | coremedia.CMTime{CMTimeValue: 20833 * 5001, CMTimeScale: coremedia.NanoSecondScale}, 58 | coremedia.CMTime{CMTimeValue: 0, CMTimeScale: 48000}, 59 | coremedia.CMTime{CMTimeValue: 5000, CMTimeScale: 48000}, 60 | float64(48008.8318464)}, 61 | } 62 | 63 | for s, tc := range testCases { 64 | calculatedSkew := coremedia.CalculateSkew(tc.startTimeClock1, tc.endTimeClock1, tc.startTimeClock2, tc.endTimeClock2) 65 | assert.Equal(t, tc.expectedValue, calculatedSkew, s) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /screencapture/coremedia/cmformatdescription.go: -------------------------------------------------------------------------------- 1 | package coremedia 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | 7 | "github.com/danielpaulus/quicktime_video_hack/screencapture/common" 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | //Those are the markers found in the hex dumps. 12 | //For convenience I have added the ASCII representation as a comment 13 | //in normal byte order and reverse byteorder (so you can find them in the hex dumps) 14 | // Note: I have just guessed what the names could be from the marker ascii, I could be wrong ;-) 15 | const ( 16 | FormatDescriptorMagic uint32 = 0x66647363 //fdsc - csdf 17 | MediaTypeVideo uint32 = 0x76696465 //vide - ediv 18 | MediaTypeSound uint32 = 0x736F756E //nuos - soun 19 | MediaTypeMagic uint32 = 0x6D646961 //mdia - aidm 20 | VideoDimensionMagic uint32 = 0x7664696D //vdim - midv 21 | CodecMagic uint32 = 0x636F6463 //codc - cdoc 22 | CodecAvc1 uint32 = 0x61766331 //avc1 - 1cva 23 | ExtensionMagic uint32 = 0x6578746E //extn - ntxe 24 | AudioStreamBasicDescriptionMagic uint32 = 0x61736264 //asdb - dbsa 25 | ) 26 | 27 | //FormatDescriptor is actually a CMFormatDescription 28 | //https://developer.apple.com/documentation/coremedia/cmformatdescription 29 | //https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.9.sdk/System/Library/Frameworks/CoreMedia.framework/Versions/A/Headers/CMFormatDescription.h 30 | type FormatDescriptor struct { 31 | MediaType uint32 32 | VideoDimensionWidth uint32 33 | VideoDimensionHeight uint32 34 | Codec uint32 35 | Extensions IndexKeyDict 36 | //PPS contains bytes of the Picture Parameter Set h264 NALu 37 | PPS []byte 38 | //SPS contains bytes of the Picture Parameter Set h264 NALu 39 | SPS []byte 40 | AudioStreamBasicDescription AudioStreamBasicDescription 41 | } 42 | 43 | //NewFormatDescriptorFromBytes parses a CMFormatDescription from bytes 44 | func NewFormatDescriptorFromBytes(data []byte) (FormatDescriptor, error) { 45 | 46 | _, remainingBytes, err := common.ParseLengthAndMagic(data, FormatDescriptorMagic) 47 | if err != nil { 48 | return FormatDescriptor{}, err 49 | } 50 | mediaType, remainingBytes, err := parseMediaType(remainingBytes) 51 | if err != nil { 52 | return FormatDescriptor{}, err 53 | } 54 | 55 | if mediaType == MediaTypeSound { 56 | return parseSoundFdsc(remainingBytes) 57 | } 58 | return parseVideoFdsc(remainingBytes) 59 | } 60 | func parseSoundFdsc(remainingBytes []byte) (FormatDescriptor, error) { 61 | 62 | length, _, err := common.ParseLengthAndMagic(remainingBytes, AudioStreamBasicDescriptionMagic) 63 | if err != nil { 64 | return FormatDescriptor{}, err 65 | } 66 | 67 | asdb, err := NewAudioStreamBasicDescriptionFromBytes(remainingBytes[8:length]) 68 | if err != nil { 69 | return FormatDescriptor{}, err 70 | } 71 | 72 | return FormatDescriptor{ 73 | MediaType: MediaTypeSound, 74 | AudioStreamBasicDescription: asdb, 75 | }, nil 76 | } 77 | func parseVideoFdsc(remainingBytes []byte) (FormatDescriptor, error) { 78 | videoDimensionWidth, videoDimensionHeight, remainingBytes, err := parseVideoDimension(remainingBytes) 79 | if err != nil { 80 | return FormatDescriptor{}, err 81 | } 82 | 83 | codec, remainingBytes, err := parseCodec(remainingBytes) 84 | if err != nil { 85 | return FormatDescriptor{}, err 86 | } 87 | 88 | extensions, err := NewIndexDictFromBytesWithCustomMarker(remainingBytes, ExtensionMagic) 89 | if err != nil { 90 | return FormatDescriptor{}, err 91 | } 92 | 93 | pps, sps := extractPPS(extensions) 94 | return FormatDescriptor{ 95 | MediaType: MediaTypeVideo, 96 | VideoDimensionHeight: videoDimensionHeight, 97 | VideoDimensionWidth: videoDimensionWidth, 98 | Codec: codec, 99 | Extensions: extensions, //doc on extensions at the bottom of: https://developer.apple.com/documentation/coremedia/cmformatdescription?language=objc 100 | PPS: pps, 101 | SPS: sps, 102 | }, nil 103 | } 104 | 105 | func extractPPS(dict IndexKeyDict) ([]byte, []byte) { 106 | val, err := dict.getValue(49) 107 | if err != nil { 108 | logrus.Error("FDSC did not contain PPS/SPS") 109 | return make([]byte, 0), make([]byte, 0) 110 | } 111 | val, err = val.(IndexKeyDict).getValue(105) 112 | if err != nil { 113 | logrus.Error("FDSC did not contain PPS/SPS") 114 | return make([]byte, 0), make([]byte, 0) 115 | } 116 | data := val.([]byte) 117 | ppsLength := data[7] 118 | pps := data[8 : 8+ppsLength] 119 | spsLength := data[10+ppsLength] 120 | sps := data[11+ppsLength : 11+ppsLength+spsLength] 121 | return pps, sps 122 | } 123 | 124 | func parseCodec(bytes []byte) (uint32, []byte, error) { 125 | length, _, err := common.ParseLengthAndMagic(bytes, CodecMagic) 126 | if err != nil { 127 | return 0, nil, err 128 | } 129 | if length != 12 { 130 | return 0, nil, fmt.Errorf("invalid length for codec: %d", length) 131 | } 132 | codec := binary.LittleEndian.Uint32(bytes[8:]) 133 | return codec, bytes[length:], nil 134 | } 135 | 136 | func parseVideoDimension(bytes []byte) (uint32, uint32, []byte, error) { 137 | length, _, err := common.ParseLengthAndMagic(bytes, VideoDimensionMagic) 138 | if err != nil { 139 | return 0, 0, nil, err 140 | } 141 | if length != 16 { 142 | return 0, 0, nil, fmt.Errorf("invalid length for video dimension: %d", length) 143 | } 144 | width := binary.LittleEndian.Uint32(bytes[8:]) 145 | height := binary.LittleEndian.Uint32(bytes[12:]) 146 | return width, height, bytes[length:], nil 147 | } 148 | 149 | func parseMediaType(bytes []byte) (uint32, []byte, error) { 150 | length, _, err := common.ParseLengthAndMagic(bytes, MediaTypeMagic) 151 | if err != nil { 152 | return 0, nil, err 153 | } 154 | if length != 12 { 155 | return 0, nil, fmt.Errorf("invalid length for media type: %d", length) 156 | } 157 | mediaType := binary.LittleEndian.Uint32(bytes[8:]) 158 | return mediaType, bytes[length:], nil 159 | } 160 | 161 | func (fdsc FormatDescriptor) String() string { 162 | if fdsc.MediaType == MediaTypeVideo { 163 | return fmt.Sprintf( 164 | "fdsc:{MediaType:%s, VideoDimension:(%dx%d), Codec:%s, PPS:%x, SPS:%x, Extensions:%s}", 165 | readableMediaType(fdsc.MediaType), fdsc.VideoDimensionWidth, fdsc.VideoDimensionHeight, 166 | readableCodec(fdsc.Codec), fdsc.PPS, fdsc.SPS, fdsc.Extensions.String()) 167 | } 168 | return fmt.Sprintf( 169 | "fdsc:{MediaType:%s, AudioStreamBasicDescription: %s}", readableMediaType(fdsc.MediaType), fdsc.AudioStreamBasicDescription.String()) 170 | } 171 | 172 | func readableCodec(codec uint32) string { 173 | if codec == CodecAvc1 { 174 | return "AVC-1" 175 | } 176 | return fmt.Sprintf("Unknown(%x)", codec) 177 | } 178 | 179 | func readableMediaType(mediaType uint32) string { 180 | if mediaType == MediaTypeVideo { 181 | return "Video" 182 | } 183 | if mediaType == MediaTypeSound { 184 | return "Sound" 185 | } 186 | return fmt.Sprintf("Unknown(%x)", mediaType) 187 | } 188 | -------------------------------------------------------------------------------- /screencapture/coremedia/cmformatdescription_test.go: -------------------------------------------------------------------------------- 1 | package coremedia_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "io/ioutil" 6 | "log" 7 | "testing" 8 | 9 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | const ppsHex = "27640033AC5680470133E69E6E04040404" 14 | const spsHex = "28EE3CB0" 15 | 16 | func TestParseFormatDescriptor(t *testing.T) { 17 | dat, err := ioutil.ReadFile("fixtures/formatdescriptor.bin") 18 | if err != nil { 19 | log.Fatal(err) 20 | } 21 | fdsc, err := coremedia.NewFormatDescriptorFromBytes(dat) 22 | if assert.NoError(t, err) { 23 | assert.Equal(t, coremedia.MediaTypeVideo, fdsc.MediaType) 24 | assert.Equal(t, decodeSafe(ppsHex), fdsc.PPS) 25 | assert.Equal(t, decodeSafe(spsHex), fdsc.SPS) 26 | println(fdsc.String()) 27 | } 28 | } 29 | 30 | func decodeSafe(s string) []byte { 31 | data, err := hex.DecodeString(s) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | return data 36 | } 37 | 38 | func TestParseFormatDescriptorAudio(t *testing.T) { 39 | dat, err := ioutil.ReadFile("fixtures/formatdescriptor-audio.bin") 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | fdsc, err := coremedia.NewFormatDescriptorFromBytes(dat) 44 | if assert.NoError(t, err) { 45 | assert.Equal(t, coremedia.MediaTypeSound, fdsc.MediaType) 46 | println(fdsc.String()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /screencapture/coremedia/cmsamplebuf.go: -------------------------------------------------------------------------------- 1 | package coremedia 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "fmt" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/common" 9 | ) 10 | 11 | //CMItemCount is a simple typedef to int to be a bit closer to MacOS/iOS 12 | type CMItemCount = int 13 | 14 | //https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.9.sdk/System/Library/Frameworks/CoreMedia.framework/Versions/A/Headers/CMSampleBuffer.h 15 | const ( 16 | sbuf uint32 = 0x73627566 //the cmsamplebuf and only content of feed asyns 17 | opts uint32 = 0x6F707473 //output presentation timestamp? 18 | stia uint32 = 0x73746961 //sampleTimingInfoArray 19 | sdat uint32 = 0x73646174 //the nalu 20 | satt uint32 = 0x73617474 //indexkey dict with only number values, CMSampleBufferGetSampleAttachmentsArray 21 | sary uint32 = 0x73617279 //some dict with index and one boolean 22 | ssiz uint32 = 0x7373697A //samplesize in bytes, size of what is contained in sdat, sample size array i think 23 | nsmp uint32 = 0x6E736D70 //numsample so you know how many things are in the arrays 24 | 25 | cmSampleTimingInfoLength = 3 * CMTimeLengthInBytes 26 | ) 27 | 28 | //CMSampleTimingInfo is a simple struct containing 3 CMtimes: Duration, PresentationTimeStamp and DecodeTimeStamp 29 | type CMSampleTimingInfo struct { 30 | Duration CMTime /*! @field duration 31 | The duration of the sample. If a single struct applies to 32 | each of the samples, they all will have this duration. */ 33 | PresentationTimeStamp CMTime /*! @field presentationTimeStamp 34 | The time at which the sample will be presented. If a single 35 | struct applies to each of the samples, this is the presentationTime of the 36 | first sample. The presentationTime of subsequent samples will be derived by 37 | repeatedly adding the sample duration. */ 38 | DecodeTimeStamp CMTime /*! @field decodeTimeStamp 39 | The time at which the sample will be decoded. If the samples 40 | are in presentation order, this must be set to kCMTimeInvalid. */ 41 | } 42 | 43 | func (info CMSampleTimingInfo) String() string { 44 | return fmt.Sprintf("{Duration:%s, PresentationTS:%s, DecodeTS:%s}", 45 | info.Duration, info.PresentationTimeStamp, info.DecodeTimeStamp) 46 | } 47 | 48 | func (buffer CMSampleBuffer) HasSampleData() bool { 49 | return buffer.SampleData != nil 50 | } 51 | 52 | //CMSampleBuffer represents the CoreMedia class used to exchange AV SampleData and contains meta information like timestamps or 53 | //optional FormatDescriptors 54 | type CMSampleBuffer struct { 55 | OutputPresentationTimestamp CMTime 56 | FormatDescription FormatDescriptor 57 | HasFormatDescription bool 58 | NumSamples CMItemCount //nsmp 59 | SampleTimingInfoArray []CMSampleTimingInfo //stia 60 | SampleData []byte 61 | SampleSizes []int 62 | Attachments IndexKeyDict //satt 63 | Sary IndexKeyDict //sary 64 | MediaType uint32 65 | } 66 | 67 | func (buffer CMSampleBuffer) String() string { 68 | var fdscString string 69 | if buffer.HasFormatDescription { 70 | fdscString = buffer.FormatDescription.String() 71 | } else { 72 | fdscString = "none" 73 | } 74 | if buffer.MediaType == MediaTypeVideo { 75 | return fmt.Sprintf("{OutputPresentationTS:%s, NumSamples:%d, Nalus:%s, fdsc:%s, attach:%s, sary:%s, SampleTimingInfoArray:%s}", 76 | buffer.OutputPresentationTimestamp.String(), buffer.NumSamples, GetNaluDetails(buffer.SampleData), 77 | fdscString, buffer.Attachments.String(), buffer.Sary.String(), buffer.SampleTimingInfoArray[0].String()) 78 | } 79 | return fmt.Sprintf("{OutputPresentationTS:%s, NumSamples:%d, SampleSize:%d, fdsc:%s}", 80 | buffer.OutputPresentationTimestamp.String(), buffer.NumSamples, buffer.SampleSizes[0], 81 | fdscString) 82 | } 83 | 84 | //NewCMSampleBufferFromBytesAudio parses a CMSampleBuffer containing audio data. 85 | func NewCMSampleBufferFromBytesAudio(data []byte) (CMSampleBuffer, error) { 86 | return NewCMSampleBufferFromBytes(data, MediaTypeSound) 87 | } 88 | 89 | //NewCMSampleBufferFromBytesVideo parses a CMSampleBuffer containing audio video. 90 | func NewCMSampleBufferFromBytesVideo(data []byte) (CMSampleBuffer, error) { 91 | return NewCMSampleBufferFromBytes(data, MediaTypeVideo) 92 | } 93 | 94 | //NewCMSampleBufferFromBytes parses a CMSampleBuffer from a []byte assuming it begins with a 4 byte length and the 4byte magic int "sbuf" 95 | func NewCMSampleBufferFromBytes(data []byte, mediaType uint32) (CMSampleBuffer, error) { 96 | var sbuffer CMSampleBuffer 97 | sbuffer.MediaType = mediaType 98 | sbuffer.HasFormatDescription = false 99 | length, remainingBytes, err := common.ParseLengthAndMagic(data, sbuf) 100 | if err != nil { 101 | return sbuffer, err 102 | } 103 | if length > len(data) { 104 | return sbuffer, fmt.Errorf("less data (%d bytes) in buffer than expected (%d bytes)", len(data), length) 105 | } 106 | for len(remainingBytes) > 0 { 107 | switch binary.LittleEndian.Uint32(remainingBytes[4:]) { 108 | case opts: 109 | cmtime, err := NewCMTimeFromBytes(remainingBytes[8:]) 110 | if err != nil { 111 | return sbuffer, err 112 | } 113 | sbuffer.OutputPresentationTimestamp = cmtime 114 | remainingBytes = remainingBytes[32:] 115 | case stia: 116 | sbuffer.SampleTimingInfoArray, remainingBytes, err = parseStia(remainingBytes) 117 | if err != nil { 118 | return sbuffer, err 119 | } 120 | case sdat: 121 | length, remainingBytes, err = common.ParseLengthAndMagic(remainingBytes, sdat) 122 | if err != nil { 123 | return sbuffer, err 124 | } 125 | sbuffer.SampleData = remainingBytes[:length-8] 126 | remainingBytes = remainingBytes[length-8:] 127 | case nsmp: 128 | 129 | length, remainingBytes, err = common.ParseLengthAndMagic(remainingBytes, nsmp) 130 | if err != nil { 131 | return sbuffer, err 132 | } 133 | if length != 12 { 134 | return sbuffer, fmt.Errorf("invalid length for nsmp %d, should be 12", length) 135 | } 136 | sbuffer.NumSamples = int(binary.LittleEndian.Uint32(remainingBytes)) 137 | remainingBytes = remainingBytes[4:] 138 | case ssiz: 139 | sbuffer.SampleSizes, remainingBytes, err = parseSampleSizeArray(remainingBytes) 140 | if err != nil { 141 | return sbuffer, err 142 | } 143 | case FormatDescriptorMagic: 144 | sbuffer.HasFormatDescription = true 145 | fdscLength := binary.LittleEndian.Uint32(remainingBytes) 146 | sbuffer.FormatDescription, err = NewFormatDescriptorFromBytes(remainingBytes[:fdscLength]) 147 | if err != nil { 148 | return sbuffer, err 149 | } 150 | remainingBytes = remainingBytes[fdscLength:] 151 | case satt: 152 | attachmentsLength := binary.LittleEndian.Uint32(remainingBytes) 153 | sbuffer.Attachments, err = NewIndexDictFromBytesWithCustomMarker(remainingBytes[:attachmentsLength], satt) 154 | if err != nil { 155 | return sbuffer, err 156 | } 157 | remainingBytes = remainingBytes[attachmentsLength:] 158 | case sary: 159 | saryLength := binary.LittleEndian.Uint32(remainingBytes) 160 | sbuffer.Sary, err = NewIndexDictFromBytes(remainingBytes[8:saryLength]) 161 | remainingBytes = remainingBytes[saryLength:] 162 | default: 163 | unknownMagic := string(remainingBytes[4:8]) 164 | return sbuffer, fmt.Errorf("unknown magic type:%s (0x%x), cannot parse value %s", unknownMagic, remainingBytes[4:8], hex.Dump(remainingBytes)) 165 | } 166 | 167 | } 168 | 169 | return sbuffer, nil 170 | } 171 | 172 | func parseSampleSizeArray(data []byte) ([]int, []byte, error) { 173 | ssizLength, _, err := common.ParseLengthAndMagic(data, ssiz) 174 | if err != nil { 175 | return nil, nil, err 176 | } 177 | ssizLength -= 8 178 | numEntries, modulus := ssizLength/4, ssizLength%4 179 | if modulus != 0 { 180 | return nil, nil, fmt.Errorf("error parsing samplesizearray, too many bytes: %d", modulus) 181 | } 182 | result := make([]int, numEntries) 183 | data = data[8:] 184 | for i := 0; i < numEntries; i++ { 185 | index := 4 * i 186 | result[i] = int(binary.LittleEndian.Uint32(data[index+i*4:])) 187 | } 188 | return result, data[ssizLength:], nil 189 | } 190 | 191 | func parseStia(data []byte) ([]CMSampleTimingInfo, []byte, error) { 192 | stiaLength, _, err := common.ParseLengthAndMagic(data, stia) 193 | if err != nil { 194 | return nil, nil, err 195 | } 196 | stiaLength -= 8 197 | 198 | numEntries, modulus := stiaLength/cmSampleTimingInfoLength, stiaLength%cmSampleTimingInfoLength 199 | if modulus != 0 { 200 | return nil, nil, fmt.Errorf("error parsing stia, too many bytes: %d", modulus) 201 | } 202 | result := make([]CMSampleTimingInfo, numEntries) 203 | data = data[8:] 204 | for i := 0; i < numEntries; i++ { 205 | index := i * cmSampleTimingInfoLength 206 | duration, err := NewCMTimeFromBytes(data[index:]) 207 | if err != nil { 208 | return nil, nil, err 209 | } 210 | presentationTimeStamp, err := NewCMTimeFromBytes(data[CMTimeLengthInBytes+index:]) 211 | if err != nil { 212 | return nil, nil, err 213 | } 214 | decodeTimeStamp, err := NewCMTimeFromBytes(data[2*CMTimeLengthInBytes+index:]) 215 | if err != nil { 216 | return nil, nil, err 217 | } 218 | 219 | result[i] = CMSampleTimingInfo{ 220 | Duration: duration, 221 | PresentationTimeStamp: presentationTimeStamp, 222 | DecodeTimeStamp: decodeTimeStamp, 223 | } 224 | } 225 | return result, data[stiaLength:], nil 226 | } 227 | -------------------------------------------------------------------------------- /screencapture/coremedia/cmsamplebuf_test.go: -------------------------------------------------------------------------------- 1 | package coremedia_test 2 | 3 | import ( 4 | "encoding/binary" 5 | "io/ioutil" 6 | "log" 7 | "testing" 8 | 9 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestFeedNoSdat(t *testing.T) { 14 | dat, err := ioutil.ReadFile("../packet/fixtures/asyn-feed-ttas-only") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | sbufPacket, err := coremedia.NewCMSampleBufferFromBytesVideo(dat[20:]) 19 | 20 | if assert.NoError(t, err) { 21 | assert.Equal(t, false, sbufPacket.HasFormatDescription) 22 | 23 | } 24 | print(sbufPacket.String()) 25 | } 26 | 27 | func TestUnknownMagic(t *testing.T) { 28 | dat, err := ioutil.ReadFile("../packet/fixtures/asyn-feed-ttas-only") 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | binary.LittleEndian.PutUint32(dat[32:], 0x75756c6c) 33 | 34 | _, err = coremedia.NewCMSampleBufferFromBytesVideo(dat[20:]) 35 | 36 | if assert.Error(t, err) { 37 | assert.Contains(t, err.Error(), "lluu") 38 | } 39 | 40 | } 41 | 42 | func TestCMSampleBuffer(t *testing.T) { 43 | dat, err := ioutil.ReadFile("../packet/fixtures/asyn-feed") 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | sbufPacket, err := coremedia.NewCMSampleBufferFromBytesVideo(dat[20:]) 48 | 49 | if assert.NoError(t, err) { 50 | assert.Equal(t, true, sbufPacket.HasFormatDescription) 51 | assert.Equal(t, coremedia.KCMTimeFlagsHasBeenRounded, sbufPacket.OutputPresentationTimestamp.CMTimeFlags) 52 | assert.Equal(t, uint64(0x176a7), sbufPacket.OutputPresentationTimestamp.Seconds()) 53 | assert.Equal(t, 1, len(sbufPacket.SampleTimingInfoArray)) 54 | assert.Equal(t, uint64(0), sbufPacket.SampleTimingInfoArray[0].Duration.Seconds()) 55 | assert.Equal(t, uint64(0x176a7), sbufPacket.SampleTimingInfoArray[0].PresentationTimeStamp.Seconds()) 56 | assert.Equal(t, uint64(0), sbufPacket.SampleTimingInfoArray[0].DecodeTimeStamp.Seconds()) 57 | assert.Equal(t, 90750, len(sbufPacket.SampleData)) 58 | assert.Equal(t, 1, sbufPacket.NumSamples) 59 | assert.Equal(t, 1, len(sbufPacket.SampleSizes)) 60 | assert.Equal(t, 90750, sbufPacket.SampleSizes[0]) 61 | assert.Equal(t, 4, len(sbufPacket.Attachments.Entries)) 62 | assert.Equal(t, 1, len(sbufPacket.Sary.Entries)) 63 | } 64 | print(sbufPacket.String()) 65 | } 66 | 67 | func TestCMSampleBufferNoFdsc(t *testing.T) { 68 | dat, err := ioutil.ReadFile("../packet/fixtures/asyn-feed-nofdsc") 69 | if err != nil { 70 | log.Fatal(err) 71 | } 72 | sbufPacket, err := coremedia.NewCMSampleBufferFromBytesVideo(dat[16:]) 73 | 74 | if assert.NoError(t, err) { 75 | assert.Equal(t, false, sbufPacket.HasFormatDescription) 76 | assert.Equal(t, coremedia.KCMTimeFlagsHasBeenRounded, sbufPacket.OutputPresentationTimestamp.CMTimeFlags) 77 | assert.Equal(t, uint64(0x44b82fa09), sbufPacket.OutputPresentationTimestamp.Seconds()) 78 | assert.Equal(t, 1, len(sbufPacket.SampleTimingInfoArray)) 79 | assert.Equal(t, uint64(0), sbufPacket.SampleTimingInfoArray[0].Duration.Seconds()) 80 | assert.Equal(t, uint64(0x44b82fa09), sbufPacket.SampleTimingInfoArray[0].PresentationTimeStamp.Seconds()) 81 | assert.Equal(t, uint64(0), sbufPacket.SampleTimingInfoArray[0].DecodeTimeStamp.Seconds()) 82 | assert.Equal(t, 56604, len(sbufPacket.SampleData)) 83 | assert.Equal(t, 1, sbufPacket.NumSamples) 84 | assert.Equal(t, 1, len(sbufPacket.SampleSizes)) 85 | assert.Equal(t, 56604, sbufPacket.SampleSizes[0]) 86 | assert.Equal(t, 4, len(sbufPacket.Attachments.Entries)) 87 | assert.Equal(t, 2, len(sbufPacket.Sary.Entries)) 88 | } 89 | print(sbufPacket.String()) 90 | } 91 | 92 | func TestCMSampleBufferAudio(t *testing.T) { 93 | dat, err := ioutil.ReadFile("../packet/fixtures/asyn-eat") 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | sbufPacket, err := coremedia.NewCMSampleBufferFromBytesAudio(dat[16:]) 98 | 99 | if assert.NoError(t, err) { 100 | assert.Equal(t, true, sbufPacket.HasFormatDescription) 101 | assert.Equal(t, 1024, sbufPacket.NumSamples) 102 | assert.Equal(t, 1, len(sbufPacket.SampleSizes)) 103 | assert.Equal(t, 4, sbufPacket.SampleSizes[0]) 104 | assert.Equal(t, sbufPacket.NumSamples*sbufPacket.SampleSizes[0], len(sbufPacket.SampleData)) 105 | stringOutput := "{OutputPresentationTS:CMTime{2056/48000, flags:KCMTimeFlagsHasBeenRounded, epoch:0}, NumSamples:1024, SampleSize:4, fdsc:fdsc:{MediaType:Sound, AudioStreamBasicDescription: {SampleRate:48000.000000,FormatFlags:76,BytesPerPacket:4,FramesPerPacket:1,BytesPerFrame:4,ChannelsPerFrame:2,BitsPerChannel:16,Reserved:0}}}" 106 | assert.Equal(t, stringOutput, sbufPacket.String()) 107 | } 108 | print(sbufPacket.String()) 109 | } 110 | 111 | func TestCMSampleBufferAudioNoFdsc(t *testing.T) { 112 | dat, err := ioutil.ReadFile("../packet/fixtures/asyn-eat-nofdsc") 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | sbufPacket, err := coremedia.NewCMSampleBufferFromBytesAudio(dat[16:]) 117 | 118 | if assert.NoError(t, err) { 119 | assert.Equal(t, false, sbufPacket.HasFormatDescription) 120 | assert.Equal(t, 1024, sbufPacket.NumSamples) 121 | assert.Equal(t, 1, len(sbufPacket.SampleSizes)) 122 | assert.Equal(t, 4, sbufPacket.SampleSizes[0]) 123 | assert.Equal(t, sbufPacket.NumSamples*sbufPacket.SampleSizes[0], len(sbufPacket.SampleData)) 124 | stringOutput := "{OutputPresentationTS:CMTime{3076/48000, flags:KCMTimeFlagsHasBeenRounded, epoch:0}, NumSamples:1024, SampleSize:4, fdsc:none}" 125 | assert.Equal(t, stringOutput, sbufPacket.String()) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /screencapture/coremedia/cmtime.go: -------------------------------------------------------------------------------- 1 | package coremedia 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | ) 8 | 9 | //Constants for the CMTime struct 10 | const ( 11 | KCMTimeFlagsValid uint32 = 0x0 12 | KCMTimeFlagsHasBeenRounded uint32 = 0x1 13 | KCMTimeFlagsPositiveInfinity uint32 = 0x2 14 | KCMTimeFlagsNegativeInfinity uint32 = 0x4 15 | KCMTimeFlagsIndefinite uint32 = 0x8 16 | KCMTimeFlagsImpliedValueFlagsMask = KCMTimeFlagsPositiveInfinity | KCMTimeFlagsNegativeInfinity | KCMTimeFlagsIndefinite 17 | CMTimeLengthInBytes int = 24 18 | ) 19 | 20 | //CMTime is taken from https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.8.sdk/System/Library/Frameworks/CoreMedia.framework/Versions/A/Headers/CMTime.h 21 | type CMTime struct { 22 | CMTimeValue uint64 /*! @field value The value of the CMTime. value/timescale = seconds. */ 23 | CMTimeScale uint32 /*! @field timescale The timescale of the CMTime. value/timescale = seconds. */ 24 | CMTimeFlags uint32 /*! @field flags The flags, eg. kCMTimeFlags_Valid, kCMTimeFlags_PositiveInfinity, etc. */ 25 | CMTimeEpoch uint64 /*! @field epoch Differentiates between equal timestamps that are actually different because 26 | of looping, multi-item sequencing, etc. 27 | Will be used during comparison: greater epochs happen after lesser ones. 28 | Additions/subtraction is only possible within a single epoch, 29 | however, since epoch length may be unknown/variable. */ 30 | } 31 | 32 | //GetTimeForScale calculates a float64 TimeValue by rescaling this CMTime to the CMTimeScale of the given CMTime 33 | func (time CMTime) GetTimeForScale(newScaleToUse CMTime) float64 { 34 | scalingFactor := float64(newScaleToUse.CMTimeScale) / float64(time.CMTimeScale) 35 | return (float64(time.CMTimeValue) * scalingFactor) 36 | } 37 | 38 | //Seconds returns CMTimeValue/CMTimeScale and 0 when all values are 0 39 | func (time CMTime) Seconds() uint64 { 40 | //prevent division by 0 41 | if time.CMTimeValue == 0 { 42 | return 0 43 | } 44 | return time.CMTimeValue / uint64(time.CMTimeScale) 45 | } 46 | 47 | //Serialize serializes this CMTime into a given byte slice that needs to be at least of CMTimeLengthInBytes length 48 | func (time CMTime) Serialize(target []byte) error { 49 | if len(target) < CMTimeLengthInBytes { 50 | return fmt.Errorf("serializing CMTime failed, not enough space in byte slice:%d", len(target)) 51 | } 52 | binary.LittleEndian.PutUint64(target, time.CMTimeValue) 53 | binary.LittleEndian.PutUint32(target[8:], time.CMTimeScale) 54 | binary.LittleEndian.PutUint32(target[12:], time.CMTimeFlags) 55 | binary.LittleEndian.PutUint64(target[16:], time.CMTimeEpoch) 56 | return nil 57 | } 58 | 59 | //NewCMTimeFromBytes reads a CMTime struct directly from the given byte slice 60 | func NewCMTimeFromBytes(data []byte) (CMTime, error) { 61 | r := bytes.NewReader(data) 62 | var cmTime CMTime 63 | err := binary.Read(r, binary.LittleEndian, &cmTime) 64 | if err != nil { 65 | return cmTime, err 66 | } 67 | return cmTime, nil 68 | } 69 | 70 | func (time CMTime) String() string { 71 | var flags string 72 | switch time.CMTimeFlags { 73 | case KCMTimeFlagsValid: 74 | flags = "KCMTimeFlagsValid" 75 | case KCMTimeFlagsHasBeenRounded: 76 | flags = "KCMTimeFlagsHasBeenRounded" 77 | case KCMTimeFlagsPositiveInfinity: 78 | flags = "KCMTimeFlagsPositiveInfinity" 79 | case KCMTimeFlagsNegativeInfinity: 80 | flags = "KCMTimeFlagsNegativeInfinity" 81 | case KCMTimeFlagsIndefinite: 82 | flags = "KCMTimeFlagsIndefinite" 83 | case KCMTimeFlagsImpliedValueFlagsMask: 84 | flags = "KCMTimeFlagsImpliedValueFlagsMask" 85 | default: 86 | flags = "unknown" 87 | } 88 | return fmt.Sprintf("CMTime{%d/%d, flags:%s, epoch:%d}", time.CMTimeValue, time.CMTimeScale, flags, time.CMTimeEpoch) 89 | } 90 | -------------------------------------------------------------------------------- /screencapture/coremedia/cmtime_test.go: -------------------------------------------------------------------------------- 1 | package coremedia_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestScaleConversion(t *testing.T) { 11 | testCases := map[string]struct { 12 | originalTime coremedia.CMTime 13 | destinationScaleTime coremedia.CMTime 14 | expectedTime float64 15 | }{ 16 | "check zero valued CMTime works": {coremedia.CMTime{CMTimeValue: 0, CMTimeScale: coremedia.NanoSecondScale}, 17 | coremedia.CMTime{CMTimeValue: 1, CMTimeScale: 2 * coremedia.NanoSecondScale}, 18 | 0}, 19 | "doubling scale": {coremedia.CMTime{CMTimeValue: 100, CMTimeScale: coremedia.NanoSecondScale}, 20 | coremedia.CMTime{CMTimeValue: 1, CMTimeScale: 2 * coremedia.NanoSecondScale}, 21 | float64(0xC8)}, 22 | "smaller scale": {coremedia.CMTime{CMTimeValue: 100, CMTimeScale: 1}, 23 | coremedia.CMTime{CMTimeValue: 1, CMTimeScale: 48000}, 24 | float64(0x493e00)}, 25 | } 26 | 27 | for s, tc := range testCases { 28 | actualTime := tc.originalTime.GetTimeForScale(tc.destinationScaleTime) 29 | assert.Equal(t, tc.expectedTime, actualTime, s) 30 | } 31 | } 32 | 33 | func TestSeconds(t *testing.T) { 34 | time := createCmTime() 35 | assert.Equal(t, uint64(2), time.Seconds()) 36 | } 37 | 38 | func TestErrors(t *testing.T) { 39 | time := createCmTime() 40 | buffer := make([]byte, 0) 41 | err := time.Serialize(buffer) 42 | assert.Error(t, err) 43 | } 44 | 45 | func TestCodec(t *testing.T) { 46 | time := createCmTime() 47 | buffer := make([]byte, 24) 48 | err := time.Serialize(buffer) 49 | if assert.NoError(t, err) { 50 | decodedTime, err := coremedia.NewCMTimeFromBytes(buffer) 51 | if assert.NoError(t, err) { 52 | assert.Equal(t, time.CMTimeEpoch, decodedTime.CMTimeEpoch) 53 | assert.Equal(t, time.CMTimeFlags, decodedTime.CMTimeFlags) 54 | assert.Equal(t, time.CMTimeScale, decodedTime.CMTimeScale) 55 | assert.Equal(t, time.CMTimeValue, decodedTime.CMTimeValue) 56 | } 57 | } 58 | _, err = coremedia.NewCMTimeFromBytes(buffer[:8]) 59 | assert.Error(t, err) 60 | } 61 | 62 | func TestString(t *testing.T) { 63 | time := createCmTime() 64 | expected := "CMTime{1000/500, flags:KCMTimeFlagsHasBeenRounded, epoch:6}" 65 | s := time.String() 66 | assert.Equal(t, expected, s) 67 | } 68 | 69 | func createCmTime() coremedia.CMTime { 70 | return coremedia.CMTime{ 71 | CMTimeValue: 1000, 72 | CMTimeScale: 500, 73 | CMTimeFlags: coremedia.KCMTimeFlagsHasBeenRounded, 74 | CMTimeEpoch: 6, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /screencapture/coremedia/dict.go: -------------------------------------------------------------------------------- 1 | package coremedia 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/danielpaulus/quicktime_video_hack/screencapture/common" 11 | ) 12 | 13 | // Dictionary related magic marker constants 14 | const ( 15 | KeyValuePairMagic uint32 = 0x6B657976 //keyv - vyek 16 | StringKey uint32 = 0x7374726B //strk - krts 17 | IntKey uint32 = 0x6964786B //idxk - kxdi 18 | BooleanValueMagic uint32 = 0x62756C76 //bulv - vlub 19 | DictionaryMagic uint32 = 0x64696374 //dict - tcid 20 | DataValueMagic uint32 = 0x64617476 //datv - vtad 21 | StringValueMagic uint32 = 0x73747276 //strv - vrts 22 | ) 23 | 24 | //StringKeyDict a dictionary that uses strings as keys with an array of StringKeyEntry 25 | type StringKeyDict struct { 26 | Entries []StringKeyEntry 27 | } 28 | 29 | //StringKeyEntry a pair of a string key and an arbitrary value 30 | type StringKeyEntry struct { 31 | Key string 32 | Value interface{} 33 | } 34 | 35 | //IndexKeyDict a dictionary that uses uint16 as keys with an array of IndexKeyEntry 36 | type IndexKeyDict struct { 37 | Entries []IndexKeyEntry 38 | } 39 | 40 | //IndexKeyEntry is a pair of a uint16 key and an arbitrary value. 41 | type IndexKeyEntry struct { 42 | Key uint16 43 | Value interface{} 44 | } 45 | 46 | //NewIndexDictFromBytes creates a new dictionary assuming the byte array starts with the 4 byte length of the dictionary followed by "dict" as the magic marker 47 | func NewIndexDictFromBytes(data []byte) (IndexKeyDict, error) { 48 | return NewIndexDictFromBytesWithCustomMarker(data, DictionaryMagic) 49 | } 50 | 51 | //NewIndexDictFromBytesWithCustomMarker creates a new dictionary assuming the byte array starts with the 4 byte length of the dictionary followed by magic as the magic marker 52 | func NewIndexDictFromBytesWithCustomMarker(data []byte, magic uint32) (IndexKeyDict, error) { 53 | _, remainingBytes, err := common.ParseLengthAndMagic(data, magic) 54 | if err != nil { 55 | return IndexKeyDict{}, err 56 | } 57 | var slice = remainingBytes 58 | dict := IndexKeyDict{} 59 | for len(slice) != 0 { 60 | keyValuePairLength, _, err := common.ParseLengthAndMagic(slice, KeyValuePairMagic) 61 | if err != nil { 62 | return IndexKeyDict{}, err 63 | } 64 | keyValuePair := slice[8:keyValuePairLength] 65 | intDictEntry, err := parseIntDictEntry(keyValuePair) 66 | if err != nil { 67 | return dict, err 68 | } 69 | dict.Entries = append(dict.Entries, intDictEntry) 70 | slice = slice[keyValuePairLength:] 71 | } 72 | return dict, nil 73 | } 74 | 75 | //NewStringDictFromBytes creates a new dictionary assuming the byte array starts with the 4 byte length of the dictionary followed by "dict" as the magic marker 76 | func NewStringDictFromBytes(data []byte) (StringKeyDict, error) { 77 | _, remainingBytes, err := common.ParseLengthAndMagic(data, DictionaryMagic) 78 | if err != nil { 79 | return StringKeyDict{}, err 80 | } 81 | 82 | var slice = remainingBytes 83 | dict := StringKeyDict{} 84 | for len(slice) != 0 { 85 | keyValuePairLength, _, err := common.ParseLengthAndMagic(slice, KeyValuePairMagic) 86 | if err != nil { 87 | return StringKeyDict{}, err 88 | } 89 | keyValuePairData := slice[8:keyValuePairLength] 90 | parseDictEntry, err := parseEntry(keyValuePairData) 91 | if err != nil { 92 | return dict, err 93 | } 94 | dict.Entries = append(dict.Entries, parseDictEntry) 95 | slice = slice[keyValuePairLength:] 96 | } 97 | return dict, nil 98 | } 99 | 100 | func parseIntDictEntry(bytes []byte) (IndexKeyEntry, error) { 101 | key, remainingBytes, err := parseIntKey(bytes) 102 | if err != nil { 103 | return IndexKeyEntry{}, err 104 | } 105 | value, err := parseValue(remainingBytes) 106 | if err != nil { 107 | return IndexKeyEntry{}, err 108 | } 109 | return IndexKeyEntry{Key: key, Value: value}, nil 110 | } 111 | 112 | //ParseKeyValueEntry parses a byte array into a StringKeyEntry assuming the array starts with a 4 byte length followed by the "keyv" magic 113 | func ParseKeyValueEntry(data []byte) (StringKeyEntry, error) { 114 | keyValuePairLength, _, err := common.ParseLengthAndMagic(data, KeyValuePairMagic) 115 | if err != nil { 116 | return StringKeyEntry{}, err 117 | } 118 | keyValuePairData := data[8:keyValuePairLength] 119 | return parseEntry(keyValuePairData) 120 | } 121 | 122 | func parseEntry(bytes []byte) (StringKeyEntry, error) { 123 | key, remainingBytes, err := parseKey(bytes) 124 | if err != nil { 125 | return StringKeyEntry{}, err 126 | } 127 | value, err := parseValue(remainingBytes) 128 | if err != nil { 129 | return StringKeyEntry{}, err 130 | } 131 | return StringKeyEntry{Key: key, Value: value}, nil 132 | } 133 | 134 | func parseKey(bytes []byte) (string, []byte, error) { 135 | keyLength, _, err := common.ParseLengthAndMagic(bytes, StringKey) 136 | if err != nil { 137 | return "", nil, err 138 | } 139 | key := string(bytes[8:keyLength]) 140 | return key, bytes[keyLength:], nil 141 | } 142 | 143 | func parseIntKey(bytes []byte) (uint16, []byte, error) { 144 | keyLength, _, err := common.ParseLengthAndMagic(bytes, IntKey) 145 | if err != nil { 146 | return 0, nil, err 147 | } 148 | key := binary.LittleEndian.Uint16(bytes[8:]) 149 | return key, bytes[keyLength:], nil 150 | } 151 | 152 | func parseValue(bytes []byte) (interface{}, error) { 153 | valueLength := binary.LittleEndian.Uint32(bytes) 154 | if len(bytes) < int(valueLength) { 155 | return nil, fmt.Errorf("invalid value data length, cannot parse %s", hex.Dump(bytes)) 156 | } 157 | magic := binary.LittleEndian.Uint32(bytes[4:]) 158 | switch magic { 159 | case StringValueMagic: 160 | return string(bytes[8:valueLength]), nil 161 | case DataValueMagic: 162 | return bytes[8:valueLength], nil 163 | case BooleanValueMagic: 164 | return bytes[8] == 1, nil 165 | case common.NumberValueMagic: 166 | return common.NewNSNumber(bytes[8:]) 167 | case DictionaryMagic: 168 | //FIXME: that is a lazy implementation, improve please 169 | dict, err := NewStringDictFromBytes(bytes) 170 | if err != nil { 171 | return NewIndexDictFromBytes(bytes) 172 | } 173 | return dict, nil 174 | case FormatDescriptorMagic: 175 | return NewFormatDescriptorFromBytes(bytes) 176 | default: 177 | unknownMagic := string(bytes[4:8]) 178 | return nil, fmt.Errorf("unknown dictionary magic type:%s (0x%x), cannot parse value %s", unknownMagic, magic, hex.Dump(bytes)) 179 | } 180 | } 181 | 182 | func (dt StringKeyDict) String() string { 183 | sb := strings.Builder{} 184 | for _, e := range dt.Entries { 185 | appendEntry(&sb, e) 186 | } 187 | return fmt.Sprintf("StringKeyDict:[%s]", sb.String()) 188 | } 189 | 190 | func (dt IndexKeyDict) String() string { 191 | sb := strings.Builder{} 192 | for _, e := range dt.Entries { 193 | appendIndexEntry(&sb, e) 194 | } 195 | return fmt.Sprintf("IndexKeyDict:[%s]", sb.String()) 196 | } 197 | 198 | func appendIndexEntry(builder *strings.Builder, entry IndexKeyEntry) { 199 | builder.WriteString("{") 200 | builder.WriteString(fmt.Sprintf("%d", entry.Key)) 201 | builder.WriteString(" : ") 202 | valueToString(builder, entry.Value) 203 | builder.WriteString("},") 204 | } 205 | 206 | func appendEntry(builder *strings.Builder, entry StringKeyEntry) { 207 | builder.WriteString("{") 208 | builder.WriteString(entry.Key) 209 | builder.WriteString(" : ") 210 | valueToString(builder, entry.Value) 211 | builder.WriteString("},") 212 | } 213 | 214 | func valueToString(builder *strings.Builder, value interface{}) { 215 | switch value := value.(type) { 216 | case common.NSNumber: 217 | builder.WriteString(value.String()) 218 | case StringKeyDict: 219 | builder.WriteString(value.String()) 220 | case []byte: 221 | builder.WriteString(fmt.Sprintf("0x%x", value)) 222 | case FormatDescriptor: 223 | builder.WriteString(value.String()) 224 | default: 225 | builder.WriteString(fmt.Sprintf("%s", value)) 226 | } 227 | } 228 | 229 | func (dt IndexKeyDict) getValue(index uint16) (interface{}, error) { 230 | for _, entry := range dt.Entries { 231 | if entry.Key == index { 232 | return entry.Value, nil 233 | } 234 | } 235 | return nil, errors.New("not found") 236 | } 237 | -------------------------------------------------------------------------------- /screencapture/coremedia/dict_serializer.go: -------------------------------------------------------------------------------- 1 | package coremedia 2 | 3 | import ( 4 | "encoding/binary" 5 | "github.com/danielpaulus/quicktime_video_hack/screencapture/common" 6 | "log" 7 | ) 8 | 9 | //SerializeStringKeyDict serializes a StringKeyDict into a []byte 10 | func SerializeStringKeyDict(stringKeyDict StringKeyDict) []byte { 11 | buffer := make([]byte, 1024*1024) 12 | var slice = buffer[8:] 13 | var index = 0 14 | for _, entry := range stringKeyDict.Entries { 15 | keyvaluePair := slice[index+8:] 16 | keyLength := serializeKey(entry.Key, keyvaluePair) 17 | valueLength := serializeValue(entry.Value, keyvaluePair[keyLength:]) 18 | common.WriteLengthAndMagic(slice[index:], keyLength+valueLength+8, KeyValuePairMagic) 19 | index += 8 + valueLength + keyLength 20 | } 21 | dictSizePlusHeaderAndLength := index + 4 + 4 22 | common.WriteLengthAndMagic(buffer, dictSizePlusHeaderAndLength, DictionaryMagic) 23 | 24 | return buffer[:dictSizePlusHeaderAndLength] 25 | } 26 | 27 | func serializeValue(value interface{}, bytes []byte) int { 28 | switch value := value.(type) { 29 | case bool: 30 | common.WriteLengthAndMagic(bytes, 9, BooleanValueMagic) 31 | var boolValue uint32 32 | if value { 33 | boolValue = 1 34 | } 35 | binary.LittleEndian.PutUint32(bytes[8:], boolValue) 36 | return 9 37 | case common.NSNumber: 38 | numberBytes := value.ToBytes() 39 | length := len(numberBytes) + 8 40 | common.WriteLengthAndMagic(bytes, length, common.NumberValueMagic) 41 | copy(bytes[8:], numberBytes) 42 | return length 43 | case string: 44 | stringValue := value 45 | length := len(stringValue) + 8 46 | common.WriteLengthAndMagic(bytes, length, StringValueMagic) 47 | copy(bytes[8:], stringValue) 48 | return length 49 | case []byte: 50 | byteValue := value 51 | length := len(byteValue) + 8 52 | common.WriteLengthAndMagic(bytes, length, DataValueMagic) 53 | copy(bytes[8:], byteValue) 54 | return length 55 | case StringKeyDict: 56 | dictValue := SerializeStringKeyDict(value) 57 | copy(bytes, dictValue) 58 | return len(dictValue) 59 | default: 60 | log.Fatalf("Wrong type while serializing dict:%s", value) 61 | } 62 | return 0 63 | } 64 | 65 | func serializeKey(key string, bytes []byte) int { 66 | keyLength := len(key) + 8 67 | common.WriteLengthAndMagic(bytes, keyLength, StringKey) 68 | copy(bytes[8:], key) 69 | return keyLength 70 | } 71 | -------------------------------------------------------------------------------- /screencapture/coremedia/dict_serializer_test.go: -------------------------------------------------------------------------------- 1 | package coremedia_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 9 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestBooleanSerialization(t *testing.T) { 14 | dat, err := ioutil.ReadFile("fixtures/bulvalue.bin") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | stringKeyDict := coremedia.StringKeyDict{Entries: make([]coremedia.StringKeyEntry, 1)} 19 | stringKeyDict.Entries[0] = coremedia.StringKeyEntry{ 20 | Key: "Valeria", 21 | Value: true, 22 | } 23 | serializedDict := coremedia.SerializeStringKeyDict(stringKeyDict) 24 | assert.Equal(t, dat, serializedDict) 25 | } 26 | 27 | func TestFullSerialization(t *testing.T) { 28 | dictBytes, err := ioutil.ReadFile("fixtures/serialize_dict.bin") 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | serializedBytes := coremedia.SerializeStringKeyDict(packet.CreateHpa1DeviceInfoDict()) 34 | assert.Equal(t, dictBytes, serializedBytes) 35 | 36 | dictBytes2, err := ioutil.ReadFile("fixtures/dict.bin") 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | serializedBytes2 := coremedia.SerializeStringKeyDict(packet.CreateHpd1DeviceInfoDict()) 42 | assert.Equal(t, dictBytes2, serializedBytes2) 43 | 44 | } 45 | -------------------------------------------------------------------------------- /screencapture/coremedia/dict_test.go: -------------------------------------------------------------------------------- 1 | package coremedia_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/danielpaulus/quicktime_video_hack/screencapture/common" 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 9 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestIntDict(t *testing.T) { 15 | dat, err := ioutil.ReadFile("fixtures/intdict.bin") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | mydict, err := coremedia.NewIndexDictFromBytes(dat) 20 | if assert.NoError(t, err) { 21 | assert.Equal(t, 2, len(mydict.Entries)) 22 | assert.Equal(t, uint16(49), mydict.Entries[0].Key) 23 | assert.IsType(t, coremedia.IndexKeyDict{}, mydict.Entries[0].Value) 24 | nestedDict := mydict.Entries[0].Value.(coremedia.IndexKeyDict) 25 | 26 | assert.Equal(t, 1, len(nestedDict.Entries)) 27 | assert.Equal(t, uint16(105), nestedDict.Entries[0].Key) 28 | assert.Equal(t, 36, len(nestedDict.Entries[0].Value.([]byte))) 29 | 30 | assert.Equal(t, uint16(52), mydict.Entries[1].Key) 31 | assert.Equal(t, "H.264", mydict.Entries[1].Value) 32 | } 33 | } 34 | 35 | func TestBooleanEntry(t *testing.T) { 36 | dat, err := ioutil.ReadFile("fixtures/bulvalue.bin") 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | mydict, err := coremedia.NewStringDictFromBytes(dat) 41 | if assert.NoError(t, err) { 42 | assert.Equal(t, 1, len(mydict.Entries)) 43 | assert.Equal(t, "Valeria", mydict.Entries[0].Key) 44 | assert.Equal(t, true, mydict.Entries[0].Value.(bool)) 45 | } 46 | } 47 | 48 | func TestSimpleDict(t *testing.T) { 49 | dat, err := ioutil.ReadFile("fixtures/dict.bin") 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | mydict, err := coremedia.NewStringDictFromBytes(dat) 54 | if assert.NoError(t, err) { 55 | assert.Equal(t, 3, len(mydict.Entries)) 56 | assert.Equal(t, "Valeria", mydict.Entries[0].Key) 57 | assert.Equal(t, true, mydict.Entries[0].Value.(bool)) 58 | 59 | assert.Equal(t, "HEVCDecoderSupports444", mydict.Entries[1].Key) 60 | assert.Equal(t, true, mydict.Entries[1].Value.(bool)) 61 | 62 | assert.Equal(t, "DisplaySize", mydict.Entries[2].Key) 63 | displaySize := mydict.Entries[2].Value.(coremedia.StringKeyDict) 64 | assert.Equal(t, 2, len(displaySize.Entries)) 65 | 66 | assert.Equal(t, "Width", displaySize.Entries[0].Key) 67 | assert.Equal(t, float64(1920), displaySize.Entries[0].Value.(common.NSNumber).FloatValue) 68 | 69 | assert.Equal(t, "Height", displaySize.Entries[1].Key) 70 | assert.Equal(t, float64(1200), displaySize.Entries[1].Value.(common.NSNumber).FloatValue) 71 | } 72 | } 73 | 74 | func TestComplexDict(t *testing.T) { 75 | dat, err := ioutil.ReadFile("fixtures/complex_dict.bin") 76 | if err != nil { 77 | log.Fatal(err) 78 | } 79 | mydict, err := coremedia.NewStringDictFromBytes(dat) 80 | if assert.NoError(t, err) { 81 | assert.Equal(t, 3, len(mydict.Entries)) 82 | assert.IsType(t, coremedia.FormatDescriptor{}, mydict.Entries[2].Value) 83 | } 84 | } 85 | 86 | func TestStringFunction(t *testing.T) { 87 | //TODO: add an assertion 88 | print(packet.CreateHpa1DeviceInfoDict().String()) 89 | numberDict := coremedia.IndexKeyDict{Entries: make([]coremedia.IndexKeyEntry, 1)} 90 | numberDict.Entries[0] = coremedia.IndexKeyEntry{ 91 | Key: 5, 92 | Value: coremedia.FormatDescriptor{ 93 | MediaType: coremedia.MediaTypeVideo, 94 | VideoDimensionWidth: 500, 95 | VideoDimensionHeight: 500, 96 | Codec: coremedia.CodecAvc1, 97 | Extensions: coremedia.IndexKeyDict{}, 98 | }, 99 | } 100 | print(numberDict.String()) 101 | } 102 | -------------------------------------------------------------------------------- /screencapture/coremedia/fixtures/adsb-from-fdsc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/coremedia/fixtures/adsb-from-fdsc -------------------------------------------------------------------------------- /screencapture/coremedia/fixtures/adsb-from-hpa-dict.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/coremedia/fixtures/adsb-from-hpa-dict.bin -------------------------------------------------------------------------------- /screencapture/coremedia/fixtures/bulvalue.bin: -------------------------------------------------------------------------------- 1 | (tcid vyekkrtsValeria vlub -------------------------------------------------------------------------------- /screencapture/coremedia/fixtures/complex_dict.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/coremedia/fixtures/complex_dict.bin -------------------------------------------------------------------------------- /screencapture/coremedia/fixtures/dict.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/coremedia/fixtures/dict.bin -------------------------------------------------------------------------------- /screencapture/coremedia/fixtures/formatdescriptor-audio.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/coremedia/fixtures/formatdescriptor-audio.bin -------------------------------------------------------------------------------- /screencapture/coremedia/fixtures/formatdescriptor.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/coremedia/fixtures/formatdescriptor.bin -------------------------------------------------------------------------------- /screencapture/coremedia/fixtures/intdict.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/coremedia/fixtures/intdict.bin -------------------------------------------------------------------------------- /screencapture/coremedia/fixtures/rply.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/coremedia/fixtures/rply.bin -------------------------------------------------------------------------------- /screencapture/coremedia/fixtures/serialize_dict.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/coremedia/fixtures/serialize_dict.bin -------------------------------------------------------------------------------- /screencapture/coremedia/nalutypetable.go: -------------------------------------------------------------------------------- 1 | package coremedia 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | //This code is just for printing human readable details about h264 nalus. 10 | //check out these resources if you want to know more about what nalus are: 11 | //https://yumichan.net/video-processing/video-compression/introduction-to-h264-nal-unit/ 12 | //https://www.semanticscholar.org/paper/Multiplexing-the-elementary-streams-of-H.264-video-Siddaraju-Rao/c7b0e625198b663be9d61c3ec7e1ec341627168c/figure/0 13 | 14 | //Table Returns a table containing all h264 nalu types 15 | func Table() []string { 16 | return []string{"unspecified", "coded slice", "data partition A", 17 | "data partition B", "data partition C", "IDR", "SEI", "sequence parameter set", "picture parameter set", 18 | "access unit delim", "end of seq", "end of stream", "filler data", 19 | "extended", "extended", "extended", "extended", "extended", "extended", "extended", "extended", "extended", "extended", 20 | "undefined", "undefined", "undefined", "undefined", "undefined", "undefined", "undefined", "undefined"} 21 | } 22 | 23 | var naluTypes = Table() 24 | 25 | //GetNaluDetails creates a string containing length and type of a h264-nalu. 26 | func GetNaluDetails(nalu []byte) string { 27 | slice := nalu 28 | sb := strings.Builder{} 29 | sb.WriteString("[") 30 | for len(slice) > 0 { 31 | length := binary.BigEndian.Uint32(slice) 32 | sb.WriteString(fmt.Sprintf("{len:%d type:%s},", length, getType(slice[4]))) 33 | slice = slice[length+4:] 34 | } 35 | sb.WriteString("]") 36 | return sb.String() 37 | } 38 | 39 | func getType(anInt byte) string { 40 | combiner := 0x1f 41 | result := combiner & int(anInt) 42 | return naluTypes[result] 43 | } 44 | -------------------------------------------------------------------------------- /screencapture/coremedia/wav_format.go: -------------------------------------------------------------------------------- 1 | package coremedia 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "os" 7 | ) 8 | 9 | /* 10 | Thank you http://soundfile.sapp.org/doc/WaveFormat/ for the amazing explanation of the WAV file format: 11 | 12 | The canonical WAVE format starts with the RIFF header: 13 | 14 | 0 4 ChunkID Contains the letters "RIFF" in ASCII form 15 | (0x52494646 big-endian form). 16 | 4 4 ChunkSize 36 + SubChunk2Size, or more precisely: 17 | 4 + (8 + SubChunk1Size) + (8 + SubChunk2Size) 18 | This is the size of the rest of the chunk 19 | following this number. This is the size of the 20 | entire file in bytes minus 8 bytes for the 21 | two fields not included in this count: 22 | ChunkID and ChunkSize. 23 | 8 4 Format Contains the letters "WAVE" 24 | (0x57415645 big-endian form). 25 | 26 | */ 27 | type riffHeader struct { 28 | ChunkID uint32 29 | ChunkSize uint32 30 | Format uint32 31 | } 32 | 33 | //newRiffHeader get a RIFF header set up for creating a WAVE file 34 | func newRiffHeader(size int) riffHeader { 35 | return riffHeader{ChunkID: 0x46464952, Format: 0x45564157, ChunkSize: uint32(36 + size)} 36 | } 37 | 38 | //serialize this RiffHeader into the given target bytes.Buffer 39 | func (rh riffHeader) serialize(target *bytes.Buffer) error { 40 | return binary.Write(target, binary.LittleEndian, rh) 41 | } 42 | 43 | /* 44 | The "WAVE" format consists of two subchunks: "fmt " and "data": 45 | The "fmt " subchunk describes the sound data's format: 46 | 47 | 12 4 Subchunk1ID Contains the letters "fmt " 48 | (0x666d7420 big-endian form). 49 | 16 4 Subchunk1Size 16 for PCM. This is the size of the 50 | rest of the Subchunk which follows this number. 51 | 20 2 AudioFormat PCM = 1 (i.e. Linear quantization) 52 | Values other than 1 indicate some 53 | form of compression. 54 | 22 2 NumChannels Mono = 1, Stereo = 2, etc. 55 | 24 4 SampleRate 8000, 44100, etc. 56 | 28 4 ByteRate == SampleRate * NumChannels * BitsPerSample/8 57 | 32 2 BlockAlign == NumChannels * BitsPerSample/8 58 | The number of bytes for one sample including 59 | all channels. I wonder what happens when 60 | this number isn't an integer? 61 | 34 2 BitsPerSample 8 bits = 8, 16 bits = 16, etc. 62 | 2 ExtraParamSize if PCM, then doesn't exist 63 | X ExtraParams space for extra parameters 64 | */ 65 | type fmtSubChunk struct { 66 | SubChunkID uint32 67 | SubChunkSize uint32 68 | AudioFormat uint16 69 | NumChannels uint16 70 | SampleRate uint32 71 | ByteRate uint32 72 | BlockAlign uint16 73 | BitsPerSample uint16 74 | } 75 | 76 | //NewFmtSubChunk generates the Fmt Subchunk for creating a WAV file 77 | func newFmtSubChunk() fmtSubChunk { 78 | result := fmtSubChunk{SubChunkID: 0x20746d66, SubChunkSize: 16, AudioFormat: 1, NumChannels: 2, SampleRate: 48000, BitsPerSample: 16} 79 | result.ByteRate = result.SampleRate * uint32(result.NumChannels) * uint32(result.BitsPerSample) / 8 80 | result.BlockAlign = result.NumChannels * (result.BitsPerSample / 8) 81 | return result 82 | } 83 | 84 | //Serialize this RiffHeader into the given target bytes.Buffer 85 | func (fmsc fmtSubChunk) serialize(target *bytes.Buffer) error { 86 | return binary.Write(target, binary.LittleEndian, fmsc) 87 | } 88 | 89 | /* 90 | The "data" subchunk contains the size of the data and the actual sound: 91 | 92 | 36 4 Subchunk2ID Contains the letters "data" 93 | (0x64617461 big-endian form). 94 | 40 4 Subchunk2Size == NumSamples * NumChannels * BitsPerSample/8 95 | This is the number of bytes in the data. 96 | You can also think of this as the size 97 | of the read of the subchunk following this 98 | number. 99 | 44 * Data The actual sound data. 100 | */ 101 | func writeWavDataSubChunkHeader(target *bytes.Buffer, dataLength int) error { 102 | err := binary.Write(target, binary.BigEndian, uint32(0x64617461)) 103 | if err != nil { 104 | return err 105 | } 106 | err = binary.Write(target, binary.LittleEndian, uint32(dataLength)) 107 | if err != nil { 108 | return err 109 | } 110 | return nil 111 | } 112 | 113 | //WriteWavHeader creates a wave file header using the given length and writes it at the BEGINNING of the wavFile. 114 | //Please make sure that the file has enough zero bytes before the audio data. 115 | func WriteWavHeader(length int, wavFile *os.File) error { 116 | headerBytes, err := GetWavHeaderBytes(length) 117 | if err != nil { 118 | return err 119 | } 120 | _, err = wavFile.WriteAt(headerBytes, 0) 121 | return err 122 | } 123 | 124 | //GetWavHeaderBytes creates a byte slice containing a valid wav header using the supplied length. 125 | func GetWavHeaderBytes(length int) ([]byte, error) { 126 | buffer := bytes.NewBuffer(make([]byte, 100)) 127 | buffer.Reset() 128 | 129 | riffHeader := newRiffHeader(length) 130 | err := riffHeader.serialize(buffer) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | fmtSubChunk := newFmtSubChunk() 136 | err = fmtSubChunk.serialize(buffer) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | err = writeWavDataSubChunkHeader(buffer, length) 142 | if err != nil { 143 | return nil, err 144 | } 145 | return buffer.Bytes(), nil 146 | } 147 | -------------------------------------------------------------------------------- /screencapture/coremedia/wav_format_test.go: -------------------------------------------------------------------------------- 1 | package coremedia_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "testing" 9 | 10 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | const expectedBytes = "524946461802000057415645666d7420100000000100020080bb000000ee02000400100064617461f401000044616e69656c" 15 | 16 | func TestWavHeaderWrittenCorrectly(t *testing.T) { 17 | 18 | headerPlaceholder := make([]byte, 44) 19 | 20 | file, err := ioutil.TempFile("", "golangtemp*") 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | defer func() { 25 | err = file.Close() 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | err = os.Remove(file.Name()) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | }() 34 | 35 | _, err = file.Write(headerPlaceholder) 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | _, err = file.Write(([]byte)("Daniel")) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | err = coremedia.WriteWavHeader(500, file) 45 | if assert.NoError(t, err) { 46 | 47 | dat, err := ioutil.ReadFile(file.Name()) 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | expectedBytes, _ := hex.DecodeString(expectedBytes) 52 | assert.Equal(t, expectedBytes, dat) 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /screencapture/diagnostics/consumer.go: -------------------------------------------------------------------------------- 1 | package diagnostics 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "runtime" 7 | "sync" 8 | "time" 9 | 10 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | //CSVHeader contains the header for the metrics file 15 | const CSVHeader = "audioSamplesRcv, audioBytesRcv, videoSamplesRcv, videoBytesRcv, heapobjects, alloc\n" 16 | 17 | //DiagnosticsConsumer periodically logs samples received, bytes received and memory stats to a csv file. 18 | type DiagnosticsConsumer struct { 19 | outFileWriter io.Writer 20 | audioSamplesRcv uint64 21 | videoSamplesRcv uint64 22 | audioBytesRcv uint64 23 | videoBytesRcv uint64 24 | mux sync.Mutex 25 | interval time.Duration 26 | stop chan struct{} 27 | stopDone chan struct{} 28 | } 29 | 30 | //NewDiagnosticsConsumer creates a new DiagnosticsConsumer 31 | func NewDiagnosticsConsumer(outfile io.Writer, interval time.Duration) *DiagnosticsConsumer { 32 | d := &DiagnosticsConsumer{outFileWriter: outfile, interval: interval, stop: make(chan struct{}), stopDone: make(chan struct{})} 33 | go fileWriter(d) 34 | return d 35 | } 36 | 37 | func fileWriter(d *DiagnosticsConsumer) { 38 | d.outFileWriter.Write([]byte(CSVHeader)) 39 | 40 | for { 41 | 42 | select { 43 | case <-d.stop: 44 | log.Info("Stopped") 45 | close(d.stopDone) 46 | return 47 | case <-time.After(d.interval): 48 | audioSamplesRcv, audioBytesRcv, videoSamplesRcv, videoBytesRcv := readAndReset(d) 49 | heapobjects, alloc := getMemStats() 50 | csvLine := fmt.Sprintf("%d,%d,%d,%d,%d,%d\n", audioSamplesRcv, audioBytesRcv, videoSamplesRcv, videoBytesRcv, heapobjects, alloc) 51 | _, err := d.outFileWriter.Write([]byte(csvLine)) 52 | if err != nil { 53 | log.Fatalf("Failed writing to metricsfile:%+v", err) 54 | } 55 | } 56 | } 57 | } 58 | 59 | func getMemStats() (uint64, uint64) { 60 | var m runtime.MemStats 61 | runtime.ReadMemStats(&m) 62 | return m.HeapObjects, m.Alloc 63 | } 64 | 65 | func readAndReset(d *DiagnosticsConsumer) (uint64, uint64, uint64, uint64) { 66 | d.mux.Lock() 67 | defer d.mux.Unlock() 68 | audioSamplesRcv, audioBytesRcv, videoSamplesRcv, videoBytesRcv := d.audioSamplesRcv, d.audioBytesRcv, d.videoSamplesRcv, d.videoBytesRcv 69 | d.audioSamplesRcv, d.audioBytesRcv, d.videoSamplesRcv, d.videoBytesRcv = 0, 0, 0, 0 70 | return audioSamplesRcv, audioBytesRcv, videoSamplesRcv, videoBytesRcv 71 | } 72 | 73 | //Consume logs stats 74 | func (d *DiagnosticsConsumer) Consume(buf coremedia.CMSampleBuffer) error { 75 | d.mux.Lock() 76 | defer d.mux.Unlock() 77 | if buf.MediaType == coremedia.MediaTypeSound { 78 | return d.consumeAudio(buf) 79 | } 80 | 81 | return d.consumeVideo(buf) 82 | } 83 | 84 | func (d *DiagnosticsConsumer) consumeAudio(buf coremedia.CMSampleBuffer) error { 85 | d.audioSamplesRcv++ 86 | d.audioBytesRcv += uint64(len(buf.SampleData)) 87 | return nil 88 | } 89 | 90 | func (d *DiagnosticsConsumer) consumeVideo(buf coremedia.CMSampleBuffer) error { 91 | d.videoSamplesRcv++ 92 | d.videoBytesRcv += uint64(len(buf.SampleData)) 93 | return nil 94 | } 95 | 96 | //Stop writing to the csv file 97 | func (d *DiagnosticsConsumer) Stop() { 98 | close(d.stop) 99 | <-d.stopDone 100 | } 101 | -------------------------------------------------------------------------------- /screencapture/diagnostics/consumer_test.go: -------------------------------------------------------------------------------- 1 | package diagnostics_test 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 10 | "github.com/danielpaulus/quicktime_video_hack/screencapture/diagnostics" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestConsumer(t *testing.T) { 15 | //buffered channel because Stop will block otherwise 16 | waiter := WriteWaiter{make(chan []byte, 100)} 17 | 18 | d := diagnostics.NewDiagnosticsConsumer(waiter, time.Millisecond*100) 19 | header := <-waiter.written 20 | assert.Equal(t, diagnostics.CSVHeader, string(header)) 21 | audioBytes := 35 22 | videoBytes := 89 23 | audiobuf := coremedia.CMSampleBuffer{MediaType: coremedia.MediaTypeSound, SampleData: make([]byte, audioBytes)} 24 | videobuf := coremedia.CMSampleBuffer{MediaType: coremedia.MediaTypeVideo, SampleData: make([]byte, videoBytes)} 25 | d.Consume(audiobuf) 26 | d.Consume(videobuf) 27 | var result []string 28 | for { 29 | data := <-waiter.written 30 | result = strings.Split(string(data), ",") 31 | if result[0] == "1" { 32 | break 33 | } 34 | } 35 | d.Stop() 36 | 37 | assert.Equal(t, result[0], "1") 38 | assert.Equal(t, result[1], strconv.Itoa(audioBytes)) 39 | assert.Equal(t, result[2], "1") 40 | assert.Equal(t, result[3], strconv.Itoa(videoBytes)) 41 | } 42 | 43 | type WriteWaiter struct { 44 | written chan []byte 45 | } 46 | 47 | func (w WriteWaiter) Write(p []byte) (n int, err error) { 48 | w.written <- p 49 | return len(p), nil 50 | } 51 | -------------------------------------------------------------------------------- /screencapture/discovery.go: -------------------------------------------------------------------------------- 1 | package screencapture 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/google/gousb" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | //IosDevice contains a gousb.Device pointer for a found device and some additional info like the device usbSerial 13 | type IosDevice struct { 14 | SerialNumber string 15 | ProductName string 16 | UsbMuxConfigIndex int 17 | QTConfigIndex int 18 | VID gousb.ID 19 | PID gousb.ID 20 | UsbInfo string 21 | } 22 | 23 | //OpenDevice finds a gousb.Device by using the provided iosDevice.SerialNumber. It returns an open device handle. 24 | //Opening using VID and PID is not specific enough, as different iOS devices can have identical VID/PID combinations. 25 | func OpenDevice(ctx *gousb.Context, iosDevice IosDevice) (*gousb.Device, error) { 26 | deviceList, err := ctx.OpenDevices(func(desc *gousb.DeviceDesc) bool { 27 | return true 28 | }) 29 | 30 | if err != nil { 31 | log.Warn("Error opening usb devices", err) 32 | } 33 | var usbDevice *gousb.Device = nil 34 | for _, device := range deviceList { 35 | sn, err := device.SerialNumber() 36 | if err != nil { 37 | log.Warn("Error retrieving Serialnumber", err) 38 | } 39 | if sn == iosDevice.SerialNumber { 40 | usbDevice = device 41 | } else { 42 | device.Close() 43 | } 44 | } 45 | 46 | if usbDevice == nil { 47 | return nil, fmt.Errorf("Unable to find device:%+v", iosDevice) 48 | } 49 | return usbDevice, nil 50 | } 51 | 52 | //ReOpen creates a new Ios device, opening it using VID and PID, using the given context 53 | func (d IosDevice) ReOpen(ctx *gousb.Context) (IosDevice, error) { 54 | 55 | dev, err := OpenDevice(ctx, d) 56 | if err != nil { 57 | return IosDevice{}, err 58 | } 59 | idev, err := mapToIosDevice([]*gousb.Device{dev}) 60 | if err != nil { 61 | return IosDevice{}, err 62 | } 63 | return idev[0], nil 64 | } 65 | 66 | const ( 67 | //UsbMuxSubclass is the subclass used for USBMux USB configuration. 68 | UsbMuxSubclass = gousb.ClassApplication 69 | //QuicktimeSubclass is the subclass used for the Quicktime USB configuration. 70 | QuicktimeSubclass gousb.Class = 0x2A 71 | ) 72 | 73 | // FindIosDevices finds iOS devices connected on USB ports by looking for their 74 | // USBMux compatible Bulk Endpoints 75 | func FindIosDevices() ([]IosDevice, error) { 76 | ctx, cleanUp := createContext() 77 | defer cleanUp() 78 | return findIosDevices(ctx, isValidIosDevice) 79 | } 80 | 81 | func createContext() (*gousb.Context, func()) { 82 | ctx := gousb.NewContext() 83 | log.Debugf("Opened usbcontext:%v", ctx) 84 | cleanUp := func() { 85 | err := ctx.Close() 86 | if err != nil { 87 | log.Fatalf("Error closing usb context: %v", ctx) 88 | } 89 | } 90 | return ctx, cleanUp 91 | } 92 | 93 | // FindIosDevice finds a iOS device by usbSerial or picks the first one if usbSerial == "" 94 | func FindIosDevice(usbSerial string) (IosDevice, error) { 95 | ctx, cleanUp := createContext() 96 | defer cleanUp() 97 | list, err := findIosDevices(ctx, isValidIosDevice) 98 | if err != nil { 99 | return IosDevice{}, err 100 | } 101 | if len(list) == 0 { 102 | return IosDevice{}, errors.New("no iOS devices are connected to this host") 103 | } 104 | if usbSerial == "" { 105 | log.Infof("no usbSerial specified, using '%s'", list[0].SerialNumber) 106 | return list[0], nil 107 | } 108 | for _, device := range list { 109 | if usbSerial == device.SerialNumber { 110 | return device, nil 111 | } 112 | } 113 | return IosDevice{}, fmt.Errorf("device with usbSerial:'%s' not found", usbSerial) 114 | } 115 | 116 | func findIosDevices(ctx *gousb.Context, validDeviceChecker func(desc *gousb.DeviceDesc) bool) ([]IosDevice, error) { 117 | devices, err := ctx.OpenDevices(func(desc *gousb.DeviceDesc) bool { 118 | // this function is called for every device present. 119 | // Returning true means the device should be opened. 120 | return validDeviceChecker(desc) 121 | }) 122 | if err != nil { 123 | return nil, err 124 | } 125 | iosDevices, err := mapToIosDevice(devices) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | return iosDevices, nil 131 | } 132 | 133 | func mapToIosDevice(devices []*gousb.Device) ([]IosDevice, error) { 134 | iosDevices := make([]IosDevice, len(devices)) 135 | for i, d := range devices { 136 | log.Debugf("Getting serial for: %s", d.String()) 137 | serial, err := d.SerialNumber() 138 | log.Debug("Got serial" + serial) 139 | if err != nil { 140 | return nil, err 141 | } 142 | product, err := d.Product() 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | muxConfigIndex, qtConfigIndex := findConfigurations(d.Desc) 148 | iosDevice := IosDevice{serial, product, muxConfigIndex, qtConfigIndex, d.Desc.Vendor, d.Desc.Product, d.String()} 149 | d.Close() 150 | iosDevices[i] = iosDevice 151 | 152 | } 153 | return iosDevices, nil 154 | } 155 | 156 | //PrintDeviceDetails returns a list of device details ready to be JSON converted. 157 | func PrintDeviceDetails(devices []IosDevice) []map[string]interface{} { 158 | result := make([]map[string]interface{}, len(devices)) 159 | for k, device := range devices { 160 | result[k] = device.DetailsMap() 161 | } 162 | return result 163 | } 164 | 165 | func isValidIosDevice(desc *gousb.DeviceDesc) bool { 166 | muxConfigIndex, _ := findConfigurations(desc) 167 | if muxConfigIndex == -1 { 168 | return false 169 | } 170 | return true 171 | } 172 | 173 | func isValidIosDeviceWithActiveQTConfig(desc *gousb.DeviceDesc) bool { 174 | _, qtConfigIndex := findConfigurations(desc) 175 | if qtConfigIndex == -1 { 176 | return false 177 | } 178 | return true 179 | } 180 | 181 | func findConfigurations(desc *gousb.DeviceDesc) (int, int) { 182 | var muxConfigIndex = -1 183 | var qtConfigIndex = -1 184 | 185 | for _, v := range desc.Configs { 186 | if isMuxConfig(v) && !isQtConfig(v) { 187 | muxConfigIndex = v.Number 188 | log.Debugf("Found MuxConfig %d for Device %s", muxConfigIndex, desc.String()) 189 | } 190 | if isQtConfig(v) { 191 | qtConfigIndex = v.Number 192 | log.Debugf("Found QTConfig %d for Device %s", qtConfigIndex, desc.String()) 193 | } 194 | } 195 | return muxConfigIndex, qtConfigIndex 196 | } 197 | 198 | func isQtConfig(confDesc gousb.ConfigDesc) bool { 199 | b, _ := findInterfaceForSubclass(confDesc, QuicktimeSubclass) 200 | return b 201 | } 202 | 203 | func isMuxConfig(confDesc gousb.ConfigDesc) bool { 204 | b, _ := findInterfaceForSubclass(confDesc, UsbMuxSubclass) 205 | return b 206 | } 207 | 208 | func findInterfaceForSubclass(confDesc gousb.ConfigDesc, subClass gousb.Class) (bool, int) { 209 | for _, iface := range confDesc.Interfaces { 210 | for _, alt := range iface.AltSettings { 211 | isVendorClass := alt.Class == gousb.ClassVendorSpec 212 | isCorrectSubClass := alt.SubClass == subClass 213 | log.Debugf("iface:%v altsettings:%d isvendor:%t isub:%t", iface, len(iface.AltSettings), isVendorClass, isCorrectSubClass) 214 | if isVendorClass && isCorrectSubClass { 215 | return true, iface.Number 216 | } 217 | } 218 | } 219 | return false, -1 220 | } 221 | 222 | //IsActivated returns a boolean that is true when this device was enabled for screen mirroring and false otherwise. 223 | func (d *IosDevice) IsActivated() bool { 224 | return d.QTConfigIndex != -1 225 | } 226 | 227 | //DetailsMap contains all the info for a device in a map ready to be JSON encoded 228 | func (d *IosDevice) DetailsMap() map[string]interface{} { 229 | return map[string]interface{}{ 230 | "deviceName": d.ProductName, 231 | "usb_device_info": d.UsbInfo, 232 | "udid": Correct24CharacterSerial(d.SerialNumber), 233 | "screen_mirroring_enabled": d.IsActivated(), 234 | } 235 | } 236 | 237 | //Usually iosDevices have a 40 character USB serial which equals the usbSerial used in usbmuxd, Xcode etc. 238 | //There is an exception, some devices like the Xr and Xs have a 24 character USB serial. Usbmux, Xcode etc. 239 | //however insert a dash after the 8th character in this case. To be compatible with other MacOS X and iOS tools, 240 | //we insert the dash here as well. 241 | func Correct24CharacterSerial(usbSerial string) string { 242 | usbSerial = strings.Trim(usbSerial, "\x00") 243 | if len(usbSerial) == 24 { 244 | return fmt.Sprintf("%s-%s", usbSerial[0:8], usbSerial[8:]) 245 | } 246 | return usbSerial 247 | } 248 | 249 | const sixteenTimesZero = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 250 | 251 | //ValidateUdid checks if a given udid is 25 or 40 characters long. 252 | //25 character udids must be of format xxxxxxxx-xxxxxxxxxxxxxxxx. 253 | //Serialnumbers on the usb host contain no dashes. As a convenience ValidateUdid 254 | //returns the udid with the dash removed so it can be used 255 | //as a correct USB SerialNumber. 256 | func ValidateUdid(udid string) (string, error) { 257 | udidLength := len(udid) 258 | if !(udidLength == 25 || udidLength == 40) { 259 | return udid, fmt.Errorf("Invalid length for udid:%s UDIDs must have 25 or 40 characters", udid) 260 | } 261 | if udidLength == 25 { 262 | if strings.Index(udid, "-") != 8 { 263 | return udid, fmt.Errorf("Invalid format for udid:%s 25 char UDIDs must contain a dash at position 8", udid) 264 | } 265 | removedDash := strings.Replace(udid, "-", "", 1) 266 | 267 | return removedDash + sixteenTimesZero, nil 268 | } 269 | return udid, nil 270 | } 271 | 272 | func (d *IosDevice) String() string { 273 | return fmt.Sprintf("'%s' %s serial: %s, qt_mode:%t", d.ProductName, d.UsbInfo, d.SerialNumber, d.IsActivated()) 274 | } 275 | -------------------------------------------------------------------------------- /screencapture/discovery_test.go: -------------------------------------------------------------------------------- 1 | package screencapture_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/danielpaulus/quicktime_video_hack/screencapture" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | const iphoneXrXsStyleSerial = "xxxxxxxxxxxxxxxxxxxxxxxx\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" 12 | const iphoneXrXsStyleUdid = "xxxxxxxx-xxxxxxxxxxxxxxxx" 13 | const regularSerial = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 14 | 15 | func TestSerialConvertedToCorrectUdid(t *testing.T) { 16 | XrXSStyleDevice := screencapture.IosDevice{SerialNumber: iphoneXrXsStyleSerial} 17 | regularDevice := screencapture.IosDevice{SerialNumber: regularSerial} 18 | details := screencapture.PrintDeviceDetails([]screencapture.IosDevice{XrXSStyleDevice, regularDevice}) 19 | assert.Equal(t, 2, len(details)) 20 | assert.Equal(t, iphoneXrXsStyleUdid, details[0]["udid"]) 21 | assert.Equal(t, regularSerial, details[1]["udid"]) 22 | } 23 | 24 | func TestValidateUdid(t *testing.T) { 25 | serial, err := screencapture.ValidateUdid(iphoneXrXsStyleUdid) 26 | assert.NoError(t, err) 27 | assert.Equal(t, 40, len(serial)) 28 | assert.Equal(t, iphoneXrXsStyleSerial, serial) 29 | serial, err = screencapture.ValidateUdid(regularSerial) 30 | assert.NoError(t, err) 31 | assert.Equal(t, regularSerial, serial) 32 | 33 | _, err = screencapture.ValidateUdid(regularSerial + "toolong") 34 | assert.Error(t, err) 35 | 36 | _, err = screencapture.ValidateUdid(strings.ReplaceAll(iphoneXrXsStyleUdid, "-", "x")) 37 | assert.Error(t, err) 38 | 39 | } 40 | -------------------------------------------------------------------------------- /screencapture/gstadapter/gst_adapter_test.go: -------------------------------------------------------------------------------- 1 | package gstadapter_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/danielpaulus/quicktime_video_hack/screencapture/gstadapter" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func skipCI(t *testing.T) { 13 | if os.Getenv("CI") != "" { 14 | t.Skip("Skipping testing in CI environment") 15 | } 16 | } 17 | 18 | func TestCustomPipelineParsing(t *testing.T) { 19 | linuxCI := os.Getenv("LINUX_CI") 20 | log.Infof("linuxCI: %s", linuxCI) 21 | if linuxCI == "true" { 22 | log.Info("Skipping gstreamer test on headless containerized CI") 23 | t.SkipNow() 24 | } 25 | 26 | _, err := gstadapter.NewWithCustomPipeline("daniel") 27 | assert.Error(t, err) 28 | 29 | _, err = gstadapter.NewWithCustomPipeline("queue name=my_filesrc ! fakesink") 30 | assert.Error(t, err) 31 | 32 | _, err = gstadapter.NewWithCustomPipeline("queue name=audio_target ! fakesink") 33 | assert.Error(t, err) 34 | 35 | gsta, err := gstadapter.NewWithCustomPipeline("rtpmux name=mux ! fakesink \n queue name=audio_target ! mux.sink_0 \n queue name=video_target ! mux.sink_1") 36 | assert.NoError(t, err) 37 | assert.NotNil(t, gsta) 38 | } 39 | -------------------------------------------------------------------------------- /screencapture/gstadapter/gst_pipeline_builder_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | 3 | package gstadapter 4 | 5 | import "github.com/danielpaulus/gst" 6 | 7 | func setupLivePlayAudio(pl *gst.Pipeline) { 8 | 9 | /*hack: I do not know why, but audio on my linux box wont play when using a simple wavpars. 10 | On MAC OS it works without any problems though. A hacky workaround to get audio playing that I came up with was 11 | to encode audio into ogg/vorbis and directly decode it again. 12 | */ 13 | 14 | vorbisenc := gst.ElementFactoryMake("vorbisenc", "vorbisenc_01") 15 | checkElem(vorbisenc, "vorbisenc_01") 16 | 17 | oggmux := gst.ElementFactoryMake("oggmux", "oggmux_01") 18 | checkElem(oggmux, "oggmux_01") 19 | 20 | oggdemux := gst.ElementFactoryMake("oggdemux", "oggdemux") 21 | checkElem(oggdemux, "oggdemux") 22 | 23 | vorbisdec := gst.ElementFactoryMake("vorbisdec", "vorbisdec") 24 | checkElem(vorbisdec, "vorbisdec") 25 | 26 | audioconvert2 := gst.ElementFactoryMake("audioconvert", "audioconvert_02") 27 | checkElem(audioconvert2, "audioconvert_02") 28 | 29 | //endhack 30 | 31 | autoaudiosink := gst.ElementFactoryMake("autoaudiosink", "autoaudiosink_01") 32 | checkElem(autoaudiosink, "autoaudiosink_01") 33 | autoaudiosink.SetProperty("sync", false) 34 | 35 | pl.Add(vorbisenc, oggmux, oggdemux, vorbisdec, audioconvert2, autoaudiosink) 36 | pl.GetByName("queue2").Link(vorbisenc) 37 | 38 | vorbisenc.Link(vorbisdec) 39 | vorbisdec.Link(audioconvert2) 40 | 41 | audioconvert2.Link(autoaudiosink) 42 | 43 | } 44 | 45 | func setUpVideoPipeline(pl *gst.Pipeline) *gst.AppSrc { 46 | asrc := gst.NewAppSrc("my-video-src") 47 | asrc.SetProperty("is-live", true) 48 | 49 | queue1 := gst.ElementFactoryMake("queue", "queue_11") 50 | checkElem(queue1, "queue_11") 51 | 52 | h264parse := gst.ElementFactoryMake("h264parse", "h264parse_01") 53 | checkElem(h264parse, "h264parse") 54 | 55 | avdec_h264 := gst.ElementFactoryMake("avdec_h264", "avdec_h264_01") 56 | checkElem(avdec_h264, "avdec_h264_01") 57 | 58 | queue2 := gst.ElementFactoryMake("queue", "queue_12") 59 | checkElem(queue2, "queue_12") 60 | 61 | videoconvert := gst.ElementFactoryMake("videoconvert", "videoconvert_01") 62 | checkElem(videoconvert, "videoconvert_01") 63 | 64 | queue3 := gst.ElementFactoryMake("queue", "queue_13") 65 | checkElem(queue3, "queue_13") 66 | 67 | sink := gst.ElementFactoryMake("xvimagesink", "xvimagesink_01") 68 | checkElem(sink, "xvimagesink01") 69 | sink.SetProperty("sync", false) //see gst_adapter_macos comment 70 | 71 | pl.Add(asrc.AsElement(), queue1, h264parse, avdec_h264, queue2, videoconvert, queue3, sink) 72 | 73 | asrc.Link(queue1) 74 | queue1.Link(h264parse) 75 | h264parse.Link(avdec_h264) 76 | avdec_h264.Link(queue2) 77 | queue2.Link(videoconvert) 78 | videoconvert.Link(queue3) 79 | queue3.Link(sink) 80 | return asrc 81 | } 82 | -------------------------------------------------------------------------------- /screencapture/gstadapter/gst_pipeline_builder_mac.go: -------------------------------------------------------------------------------- 1 | // +build darwin 2 | 3 | package gstadapter 4 | 5 | import "github.com/danielpaulus/gst" 6 | 7 | func setupLivePlayAudio(pl *gst.Pipeline) { 8 | autoaudiosink := gst.ElementFactoryMake("autoaudiosink", "autoaudiosink_01") 9 | checkElem(autoaudiosink, "autoaudiosink_01") 10 | autoaudiosink.SetProperty("sync", false) 11 | pl.Add(autoaudiosink) 12 | pl.GetByName("queue2").Link(autoaudiosink) 13 | } 14 | 15 | func setUpVideoPipeline(pl *gst.Pipeline) *gst.AppSrc { 16 | asrc := gst.NewAppSrc("my-video-src") 17 | asrc.SetProperty("is-live", true) 18 | 19 | queue1 := gst.ElementFactoryMake("queue", "queue_11") 20 | checkElem(queue1, "queue_11") 21 | 22 | h264parse := gst.ElementFactoryMake("h264parse", "h264parse_01") 23 | checkElem(h264parse, "h264parse") 24 | 25 | avdecH264 := gst.ElementFactoryMake("vtdec", "vtdec_01") 26 | checkElem(avdecH264, "vtdec_01") 27 | 28 | queue2 := gst.ElementFactoryMake("queue", "queue_12") 29 | checkElem(queue2, "queue_12") 30 | 31 | videoconvert := gst.ElementFactoryMake("videoconvert", "videoconvert_01") 32 | checkElem(videoconvert, "videoconvert_01") 33 | 34 | queue3 := gst.ElementFactoryMake("queue", "queue_13") 35 | checkElem(queue3, "queue_13") 36 | 37 | sink := gst.ElementFactoryMake("autovideosink", "autovideosink_01") 38 | // setting this to true, creates extremely choppy video 39 | // I probably messed up something regarding the time stamps 40 | sink.SetProperty("sync", false) 41 | checkElem(sink, "autovideosink_01") 42 | 43 | pl.Add(asrc.AsElement(), queue1, h264parse, avdecH264, queue2, videoconvert, queue3, sink) 44 | 45 | asrc.Link(queue1) 46 | queue1.Link(h264parse) 47 | h264parse.Link(avdecH264) 48 | avdecH264.Link(queue2) 49 | queue2.Link(videoconvert) 50 | videoconvert.Link(queue3) 51 | queue3.Link(sink) 52 | return asrc 53 | } 54 | -------------------------------------------------------------------------------- /screencapture/interfaces.go: -------------------------------------------------------------------------------- 1 | package screencapture 2 | 3 | import "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 4 | 5 | //CmSampleBufConsumer is a simple interface with one function that consumes CMSampleBuffers 6 | type CmSampleBufConsumer interface { 7 | Consume(buf coremedia.CMSampleBuffer) error 8 | Stop() 9 | } 10 | 11 | //UsbDataReceiver should receive USB SYNC, ASYN and PING packets with the correct length and with the 4 bytes length removed. 12 | type UsbDataReceiver interface { 13 | ReceiveData(data []byte) 14 | CloseSession() 15 | } 16 | 17 | //UsbWriter can be used to send data to a USB Endpoint 18 | type UsbWriter interface { 19 | WriteDataToUsb(data []byte) 20 | } 21 | -------------------------------------------------------------------------------- /screencapture/messageprocessor_test.go: -------------------------------------------------------------------------------- 1 | package screencapture_test 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "testing" 9 | 10 | "github.com/danielpaulus/quicktime_video_hack/screencapture" 11 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 12 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | type UsbTestDummy struct { 17 | dataReceiver chan []byte 18 | cmSampleBufConsumer chan coremedia.CMSampleBuffer 19 | } 20 | 21 | func (u UsbTestDummy) Consume(buf coremedia.CMSampleBuffer) error { 22 | u.cmSampleBufConsumer <- buf 23 | return nil 24 | } 25 | 26 | func (u UsbTestDummy) Stop() {} 27 | 28 | func (u UsbTestDummy) WriteDataToUsb(data []byte) { 29 | u.dataReceiver <- data 30 | } 31 | 32 | func TestMessageProcessorStopsOnUnknownPacket(t *testing.T) { 33 | usbDummy := UsbTestDummy{} 34 | stopChannel := make(chan interface{}) 35 | mp := screencapture.NewMessageProcessor(usbDummy, stopChannel, usbDummy, false) 36 | go func() { mp.ReceiveData(make([]byte, 4)) }() 37 | <-stopChannel 38 | } 39 | 40 | type syncTestCase struct { 41 | receivedData []byte 42 | expectedReply [][]byte 43 | description string 44 | } 45 | 46 | func TestMessageProcessorRespondsCorrectlyToSyncMessages(t *testing.T) { 47 | clokRequest := loadFromFile("clok-request")[4:] 48 | parsedClokRequest, _ := packet.NewSyncClokPacketFromBytes(clokRequest) 49 | 50 | cvrpRequest := loadFromFile("cvrp-request")[4:] 51 | parsedCvrpRequest, _ := packet.NewSyncCvrpPacketFromBytes(cvrpRequest) 52 | 53 | cwpaRequest := loadFromFile("cwpa-request1")[4:] 54 | parsedCwpaRequest, _ := packet.NewSyncCwpaPacketFromBytes(cwpaRequest) 55 | 56 | cases := []syncTestCase{ 57 | { 58 | receivedData: packet.NewPingPacketAsBytes()[4:], 59 | expectedReply: [][]byte{packet.NewPingPacketAsBytes()}, 60 | description: "Expect Ping as a response to a ping packet", 61 | }, 62 | { 63 | receivedData: loadFromFile("afmt-request")[4:], 64 | expectedReply: [][]byte{loadFromFile("afmt-reply")}, 65 | description: "Expect correct reply for afmt", 66 | }, 67 | { 68 | receivedData: clokRequest, 69 | expectedReply: [][]byte{parsedClokRequest.NewReply(parsedClokRequest.ClockRef + 0x10000)}, 70 | description: "Expect correct reply for clok", 71 | }, 72 | { 73 | receivedData: cvrpRequest, 74 | expectedReply: [][]byte{packet.AsynNeedPacketBytes(parsedCvrpRequest.DeviceClockRef), parsedCvrpRequest.NewReply(parsedCvrpRequest.DeviceClockRef + 0x1000AF)}, 75 | description: "Expect correct reply for cvrp", 76 | }, 77 | { 78 | receivedData: cwpaRequest, 79 | expectedReply: [][]byte{packet.NewAsynHpd1Packet(packet.CreateHpd1DeviceInfoDict()), 80 | packet.NewAsynHpd1Packet(packet.CreateHpd1DeviceInfoDict()), 81 | parsedCwpaRequest.NewReply(parsedCwpaRequest.DeviceClockRef + 1000), 82 | packet.NewAsynHpa1Packet(packet.CreateHpa1DeviceInfoDict(), parsedCwpaRequest.DeviceClockRef)}, 83 | description: "Expect correct reply for cwpa", 84 | }, 85 | { 86 | receivedData: loadFromFile("og-request")[4:], 87 | expectedReply: [][]byte{loadFromFile("og-reply")}, 88 | description: "Expect correct reply for og", 89 | }, 90 | { 91 | receivedData: loadFromFile("stop-request")[4:], 92 | expectedReply: [][]byte{loadFromFile("stop-reply")}, 93 | description: "Expect correct reply for stop", 94 | }, 95 | } 96 | 97 | usbDummy := UsbTestDummy{dataReceiver: make(chan []byte)} 98 | stopChannel := make(chan interface{}) 99 | mp := screencapture.NewMessageProcessorWithClockBuilder(usbDummy, stopChannel, usbDummy, 100 | func(ID uint64) coremedia.CMClock { return coremedia.NewCMClockWithHostTime(5) }, false) 101 | 102 | for _, testCase := range cases { 103 | go func() { mp.ReceiveData(testCase.receivedData) }() 104 | for _, expectedResponse := range testCase.expectedReply { 105 | response := <-usbDummy.dataReceiver 106 | assert.Equal(t, expectedResponse, response, testCase.description) 107 | } 108 | } 109 | 110 | } 111 | 112 | func TestMessageProcessorRespondsCorrectlyToTimeSyncMessages(t *testing.T) { 113 | timeBytes := loadFromFile("time-request1")[4:] 114 | timeRequest, err := packet.NewSyncTimePacketFromBytes(timeBytes) 115 | if err != nil { 116 | log.Fatal(err) 117 | } 118 | testCases := map[string]struct { 119 | receivedData []byte 120 | timeRequest packet.SyncTimePacket 121 | }{ 122 | "check on time request it sends a reply valid CMTime and correlationID": {timeBytes, timeRequest}, 123 | } 124 | 125 | usbDummy := UsbTestDummy{dataReceiver: make(chan []byte)} 126 | stopChannel := make(chan interface{}) 127 | mp := screencapture.NewMessageProcessorWithClockBuilder(usbDummy, stopChannel, usbDummy, 128 | func(ID uint64) coremedia.CMClock { return coremedia.NewCMClockWithHostTime(5) }, false) 129 | 130 | for k, testCase := range testCases { 131 | go func() { mp.ReceiveData(testCase.receivedData) }() 132 | response := <-usbDummy.dataReceiver 133 | fmt.Printf("%x", response) 134 | assert.Equal(t, uint32(len(response)), binary.LittleEndian.Uint32(response), k) 135 | assert.Equal(t, packet.ReplyPacketMagic, binary.LittleEndian.Uint32(response[4:]), k) 136 | assert.Equal(t, testCase.timeRequest.CorrelationID, binary.LittleEndian.Uint64(response[8:]), k) 137 | _, err := coremedia.NewCMTimeFromBytes(response[16:]) 138 | assert.NoError(t, err) 139 | } 140 | } 141 | 142 | func TestMessageProcessorForwardsFeed(t *testing.T) { 143 | dat, err := ioutil.ReadFile("packet/fixtures/asyn-feed") 144 | if err != nil { 145 | log.Fatal(err) 146 | } 147 | 148 | usbDummy := UsbTestDummy{dataReceiver: make(chan []byte), cmSampleBufConsumer: make(chan coremedia.CMSampleBuffer)} 149 | stopChannel := make(chan interface{}) 150 | mp := screencapture.NewMessageProcessor(usbDummy, stopChannel, usbDummy, false) 151 | go func() { mp.ReceiveData(dat[4:]) }() 152 | response := <-usbDummy.cmSampleBufConsumer 153 | expected := "{OutputPresentationTS:CMTime{95911997690984/1000000000, flags:KCMTimeFlagsHasBeenRounded, epoch:0}, NumSamples:1, Nalus:[{len:30 type:SEI},{len:90712 type:IDR},], fdsc:fdsc:{MediaType:Video, VideoDimension:(1126x2436), Codec:AVC-1, PPS:27640033ac5680470133e69e6e04040404, SPS:28ee3cb0, Extensions:IndexKeyDict:[{49 : IndexKeyDict:[{105 : 0x01640033ffe1001127640033ac5680470133e69e6e0404040401000428ee3cb0fdf8f800},]},{52 : H.264},]}, attach:IndexKeyDict:[{28 : IndexKeyDict:[{46 : Float64[2436.000000]},{47 : Float64[2436.000000]},]},{29 : Int32[0]},{26 : IndexKeyDict:[{46 : Float64[1126.000000]},{47 : Float64[2436.000000]},{45 : Float64[0.000000]},{44 : Float64[0.000000]},]},{27 : IndexKeyDict:[{46 : Float64[1126.000000]},{47 : Float64[2436.000000]},{45 : Float64[0.000000]},{44 : Float64[0.000000]},]},], sary:IndexKeyDict:[{4 : %!s(bool=false)},], SampleTimingInfoArray:{Duration:CMTime{1/60, flags:KCMTimeFlagsHasBeenRounded, epoch:0}, PresentationTS:CMTime{95911997690984/1000000000, flags:KCMTimeFlagsHasBeenRounded, epoch:0}, DecodeTS:CMTime{0/0, flags:KCMTimeFlagsValid, epoch:0}}}" 154 | 155 | assert.Equal(t, expected, response.String()) 156 | } 157 | 158 | func TestMessageProcessorShutdownMessagesAreCorrect(t *testing.T) { 159 | usbDummy := UsbTestDummy{dataReceiver: make(chan []byte), cmSampleBufConsumer: make(chan coremedia.CMSampleBuffer)} 160 | stopChannel := make(chan interface{}) 161 | mp := screencapture.NewMessageProcessor(usbDummy, stopChannel, usbDummy, false) 162 | waitCloseSessionChannel := make(chan interface{}) 163 | 164 | go func() { 165 | mp.CloseSession() 166 | var signal interface{} 167 | waitCloseSessionChannel <- signal 168 | }() 169 | expectedHPA0 := packet.NewAsynHPA0(0x0) 170 | expectedHPD0 := packet.NewAsynHPD0() 171 | hpa0 := <-usbDummy.dataReceiver 172 | hpd0 := <-usbDummy.dataReceiver 173 | 174 | assert.Equal(t, expectedHPA0, hpa0) 175 | assert.Equal(t, expectedHPD0, hpd0) 176 | 177 | go func() { 178 | mp.ReceiveData(loadFromFile("asyn-rels")[4:]) 179 | mp.ReceiveData(loadFromFile("asyn-rels")[4:]) 180 | }() 181 | 182 | assert.Equal(t, expectedHPD0, <-usbDummy.dataReceiver) 183 | <-waitCloseSessionChannel 184 | } 185 | 186 | func loadFromFile(name string) []byte { 187 | dat, err := ioutil.ReadFile("packet/fixtures/" + name) 188 | if err != nil { 189 | log.Fatal(err) 190 | } 191 | return dat 192 | } 193 | -------------------------------------------------------------------------------- /screencapture/packet/asyn.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/danielpaulus/quicktime_video_hack/screencapture/common" 7 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 8 | ) 9 | 10 | //Async Packet types 11 | const ( 12 | AsynPacketMagic uint32 = 0x6173796E 13 | FEED uint32 = 0x66656564 //These contain CMSampleBufs which contain raw h264 Nalus 14 | TJMP uint32 = 0x746A6D70 15 | SRAT uint32 = 0x73726174 //CMTimebaseSetRateAndAnchorTime https://developer.apple.com/documentation/coremedia/cmtimebase?language=objc 16 | SPRP uint32 = 0x73707270 // Set Property 17 | TBAS uint32 = 0x74626173 //TimeBase https://developer.apple.com/library/archive/qa/qa1643/_index.html 18 | RELS uint32 = 0x72656C73 19 | HPD1 uint32 = 0x68706431 //hpd1 - 1dph | For specifying/requesting the video format 20 | HPA1 uint32 = 0x68706131 //hpa1 - 1aph | For specifying/requesting the audio format 21 | NEED uint32 = 0x6E656564 //need - deen 22 | EAT uint32 = 0x65617421 //contains audio sbufs 23 | HPD0 uint32 = 0x68706430 24 | HPA0 uint32 = 0x68706130 25 | ) 26 | 27 | //NewAsynHpd1Packet creates a []byte containing a valid ASYN packet with the Hpd1 dictionary 28 | func NewAsynHpd1Packet(stringKeyDict coremedia.StringKeyDict) []byte { 29 | return newAsynDictPacket(stringKeyDict, HPD1, EmptyCFType) 30 | } 31 | 32 | //NewAsynHpa1Packet creates a []byte containing a valid ASYN packet with the Hpa1 dictionary 33 | func NewAsynHpa1Packet(stringKeyDict coremedia.StringKeyDict, clockRef CFTypeID) []byte { 34 | return newAsynDictPacket(stringKeyDict, HPA1, clockRef) 35 | } 36 | 37 | func newAsynDictPacket(stringKeyDict coremedia.StringKeyDict, subtypeMarker uint32, asynTypeHeader uint64) []byte { 38 | serialize := coremedia.SerializeStringKeyDict(stringKeyDict) 39 | length := len(serialize) + 20 40 | header := make([]byte, 20) 41 | binary.LittleEndian.PutUint32(header, uint32(length)) 42 | binary.LittleEndian.PutUint32(header[4:], AsynPacketMagic) 43 | binary.LittleEndian.PutUint64(header[8:], asynTypeHeader) 44 | binary.LittleEndian.PutUint32(header[16:], subtypeMarker) 45 | return append(header, serialize...) 46 | } 47 | 48 | //AsynNeedPacketBytes can be used to create the NEED message as soon as the clockRef from SYNC CVRP has been received. 49 | func AsynNeedPacketBytes(clockRef CFTypeID) []byte { 50 | needPacketLength := 20 51 | packet := make([]byte, needPacketLength) 52 | binary.LittleEndian.PutUint32(packet, uint32(needPacketLength)) 53 | binary.LittleEndian.PutUint32(packet[4:], AsynPacketMagic) 54 | binary.LittleEndian.PutUint64(packet[8:], clockRef) 55 | binary.LittleEndian.PutUint32(packet[16:], NEED) //need - deen 56 | return packet 57 | } 58 | 59 | //CreateHpd1DeviceInfoDict creates a dict.StringKeyDict that needs to be sent to the device before receiving a feed 60 | func CreateHpd1DeviceInfoDict() coremedia.StringKeyDict { 61 | resultDict := coremedia.StringKeyDict{Entries: make([]coremedia.StringKeyEntry, 3)} 62 | displaySizeDict := coremedia.StringKeyDict{Entries: make([]coremedia.StringKeyEntry, 2)} 63 | resultDict.Entries[0] = coremedia.StringKeyEntry{ 64 | Key: "Valeria", 65 | Value: true, 66 | } 67 | resultDict.Entries[1] = coremedia.StringKeyEntry{ 68 | Key: "HEVCDecoderSupports444", 69 | Value: true, 70 | } 71 | 72 | displaySizeDict.Entries[0] = coremedia.StringKeyEntry{ 73 | Key: "Width", 74 | Value: common.NewNSNumberFromUFloat64(1920), 75 | } 76 | displaySizeDict.Entries[1] = coremedia.StringKeyEntry{ 77 | Key: "Height", 78 | Value: common.NewNSNumberFromUFloat64(1200), 79 | } 80 | 81 | resultDict.Entries[2] = coremedia.StringKeyEntry{ 82 | Key: "DisplaySize", 83 | Value: displaySizeDict, 84 | } 85 | 86 | return resultDict 87 | } 88 | 89 | //CreateHpa1DeviceInfoDict creates a dict.StringKeyDict that needs to be sent to the device before receiving a feed 90 | func CreateHpa1DeviceInfoDict() coremedia.StringKeyDict { 91 | asbdBytes := make([]byte, 56) 92 | coremedia.DefaultAudioStreamBasicDescription().SerializeAudioStreamBasicDescription(asbdBytes) 93 | resultDict := coremedia.StringKeyDict{Entries: make([]coremedia.StringKeyEntry, 6)} 94 | resultDict.Entries[0] = coremedia.StringKeyEntry{ 95 | Key: "BufferAheadInterval", 96 | Value: common.NewNSNumberFromUFloat64(0.07300000000000001), 97 | } 98 | 99 | resultDict.Entries[1] = coremedia.StringKeyEntry{ 100 | Key: "deviceUID", 101 | Value: "Valeria", 102 | } 103 | 104 | resultDict.Entries[2] = coremedia.StringKeyEntry{ 105 | Key: "ScreenLatency", 106 | Value: common.NewNSNumberFromUFloat64(0.04), 107 | } 108 | 109 | resultDict.Entries[3] = coremedia.StringKeyEntry{ 110 | Key: "formats", 111 | Value: asbdBytes, 112 | } 113 | 114 | resultDict.Entries[4] = coremedia.StringKeyEntry{ 115 | Key: "EDIDAC3Support", 116 | Value: common.NewNSNumberFromUInt32(0), 117 | } 118 | 119 | resultDict.Entries[5] = coremedia.StringKeyEntry{ 120 | Key: "deviceName", 121 | Value: "Valeria", 122 | } 123 | return resultDict 124 | } 125 | 126 | //NewAsynHPD0 creates the bytes needed for stopping video streaming 127 | func NewAsynHPD0() []byte { 128 | length := 20 129 | data := make([]byte, length) 130 | binary.LittleEndian.PutUint32(data, uint32(length)) 131 | binary.LittleEndian.PutUint32(data[4:], AsynPacketMagic) 132 | binary.LittleEndian.PutUint64(data[8:], EmptyCFType) 133 | binary.LittleEndian.PutUint32(data[16:], HPD0) 134 | return data 135 | } 136 | 137 | //NewAsynHPA0 creates the bytes needed for stopping audio streaming 138 | func NewAsynHPA0(clockRef uint64) []byte { 139 | length := 20 140 | data := make([]byte, length) 141 | binary.LittleEndian.PutUint32(data, uint32(length)) 142 | binary.LittleEndian.PutUint32(data[4:], AsynPacketMagic) 143 | binary.LittleEndian.PutUint64(data[8:], clockRef) 144 | binary.LittleEndian.PutUint32(data[16:], HPA0) 145 | return data 146 | } 147 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_feed.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | 7 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 8 | ) 9 | 10 | //AsynCmSampleBufPacket contains a CMSampleBuffer with audio or video data 11 | type AsynCmSampleBufPacket struct { 12 | ClockRef CFTypeID 13 | CMSampleBuf coremedia.CMSampleBuffer 14 | } 15 | 16 | //NewAsynCmSampleBufPacketFromBytes parses a new AsynCmSampleBufPacket from bytes 17 | func NewAsynCmSampleBufPacketFromBytes(data []byte) (AsynCmSampleBufPacket, error) { 18 | clockRef, sBuf, err := newAsynCmSampleBufferPacketFromBytes(data) 19 | if err != nil { 20 | return AsynCmSampleBufPacket{}, err 21 | } 22 | return AsynCmSampleBufPacket{ClockRef: clockRef, CMSampleBuf: sBuf}, nil 23 | } 24 | 25 | func newAsynCmSampleBufferPacketFromBytes(data []byte) (CFTypeID, coremedia.CMSampleBuffer, error) { 26 | magic := binary.LittleEndian.Uint32(data[12:]) 27 | _, clockRef, err := ParseAsynHeader(data, magic) 28 | if err != nil { 29 | return 0, coremedia.CMSampleBuffer{}, err 30 | } 31 | 32 | var cMSampleBuf coremedia.CMSampleBuffer 33 | 34 | if magic == FEED { 35 | cMSampleBuf, err = coremedia.NewCMSampleBufferFromBytesVideo(data[16:]) 36 | if err != nil { 37 | return 0, coremedia.CMSampleBuffer{}, err 38 | } 39 | } else { 40 | cMSampleBuf, err = coremedia.NewCMSampleBufferFromBytesAudio(data[16:]) 41 | if err != nil { 42 | return 0, coremedia.CMSampleBuffer{}, err 43 | } 44 | } 45 | 46 | return clockRef, cMSampleBuf, nil 47 | } 48 | 49 | func (sp AsynCmSampleBufPacket) String() string { 50 | return fmt.Sprintf("ASYN_SBUF{ClockRef:%x, sBuf:%s}", sp.ClockRef, sp.CMSampleBuf.String()) 51 | } 52 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_feed_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 9 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | const expectedString = `ASYN_SBUF{ClockRef:7ffb5cc32f60, sBuf:{OutputPresentationTS:CMTime{95911997690984/1000000000, flags:KCMTimeFlagsHasBeenRounded, epoch:0}, NumSamples:1, Nalus:[{len:30 type:SEI},{len:90712 type:IDR},], fdsc:fdsc:{MediaType:Video, VideoDimension:(1126x2436), Codec:AVC-1, PPS:27640033ac5680470133e69e6e04040404, SPS:28ee3cb0, Extensions:IndexKeyDict:[{49 : IndexKeyDict:[{105 : 0x01640033ffe1001127640033ac5680470133e69e6e0404040401000428ee3cb0fdf8f800},]},{52 : H.264},]}, attach:IndexKeyDict:[{28 : IndexKeyDict:[{46 : Float64[2436.000000]},{47 : Float64[2436.000000]},]},{29 : Int32[0]},{26 : IndexKeyDict:[{46 : Float64[1126.000000]},{47 : Float64[2436.000000]},{45 : Float64[0.000000]},{44 : Float64[0.000000]},]},{27 : IndexKeyDict:[{46 : Float64[1126.000000]},{47 : Float64[2436.000000]},{45 : Float64[0.000000]},{44 : Float64[0.000000]},]},], sary:IndexKeyDict:[{4 : %!s(bool=false)},], SampleTimingInfoArray:{Duration:CMTime{1/60, flags:KCMTimeFlagsHasBeenRounded, epoch:0}, PresentationTS:CMTime{95911997690984/1000000000, flags:KCMTimeFlagsHasBeenRounded, epoch:0}, DecodeTS:CMTime{0/0, flags:KCMTimeFlagsValid, epoch:0}}}}` 14 | 15 | func TestFeed(t *testing.T) { 16 | dat, err := ioutil.ReadFile("fixtures/asyn-feed") 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | feedPacket, err := packet.NewAsynCmSampleBufPacketFromBytes(dat[4:]) 21 | if assert.NoError(t, err) { 22 | assert.Equal(t, uint64(0x7ffb5cc32f60), feedPacket.ClockRef) 23 | assert.Equal(t, expectedString, feedPacket.String()) 24 | assert.Equal(t, coremedia.MediaTypeVideo, feedPacket.CMSampleBuf.MediaType) 25 | } 26 | } 27 | 28 | func TestEat(t *testing.T) { 29 | dat, err := ioutil.ReadFile("fixtures/asyn-eat") 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | feedPacket, err := packet.NewAsynCmSampleBufPacketFromBytes(dat) 34 | if assert.NoError(t, err) { 35 | assert.Equal(t, uint64(0x133959728), feedPacket.ClockRef) 36 | assert.Equal(t, coremedia.MediaTypeSound, feedPacket.CMSampleBuf.MediaType) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_rels.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | //AsynRelsPacket tells us that a clock was released 8 | type AsynRelsPacket struct { 9 | ClockRef CFTypeID 10 | } 11 | 12 | //NewAsynRelsPacketFromBytes creates a new AsynRelsPacket from bytes 13 | func NewAsynRelsPacketFromBytes(data []byte) (AsynRelsPacket, error) { 14 | var packet = AsynRelsPacket{} 15 | _, clockRef, err := ParseAsynHeader(data, RELS) 16 | if err != nil { 17 | return packet, err 18 | } 19 | packet.ClockRef = clockRef 20 | 21 | return packet, nil 22 | } 23 | 24 | func (sp AsynRelsPacket) String() string { 25 | return fmt.Sprintf("ASYN_RELS{ClockRef:%x}", sp.ClockRef) 26 | } 27 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_rels_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestRels(t *testing.T) { 13 | dat, err := ioutil.ReadFile("fixtures/asyn-rels") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | relsPacket, err := packet.NewAsynRelsPacketFromBytes(dat[4:]) 18 | if assert.NoError(t, err) { 19 | assert.Equal(t, uint64(0x7fba35608a00), relsPacket.ClockRef) 20 | assert.Equal(t, "ASYN_RELS{ClockRef:7fba35608a00}", relsPacket.String()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_sprp.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 7 | ) 8 | 9 | //AsynSprpPacket seems to be a set property packet sent by the device. 10 | type AsynSprpPacket struct { 11 | ClockRef CFTypeID 12 | Property coremedia.StringKeyEntry 13 | } 14 | 15 | //NewAsynSprpPacketFromBytes creates a new AsynSprpPacket from bytes 16 | func NewAsynSprpPacketFromBytes(data []byte) (AsynSprpPacket, error) { 17 | var packet = AsynSprpPacket{} 18 | remainingBytes, clockRef, err := ParseAsynHeader(data, SPRP) 19 | if err != nil { 20 | return packet, err 21 | } 22 | packet.ClockRef = clockRef 23 | entry, err := coremedia.ParseKeyValueEntry(remainingBytes) 24 | if err != nil { 25 | return packet, err 26 | } 27 | packet.Property = entry 28 | return packet, nil 29 | } 30 | 31 | func (sp AsynSprpPacket) String() string { 32 | return fmt.Sprintf("ASYN_SPRP{ClockRef:%x, Property:{%s:%s}}", sp.ClockRef, sp.Property.Key, sp.Property.Value) 33 | } 34 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_sprp_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestSprp(t *testing.T) { 13 | dat, err := ioutil.ReadFile("fixtures/asyn-sprp") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | sprpPacket, err := packet.NewAsynSprpPacketFromBytes(dat) 18 | if assert.NoError(t, err) { 19 | assert.Equal(t, uint64(0x11123bc18), sprpPacket.ClockRef) 20 | assert.Equal(t, "ObeyEmptyMediaMarkers", sprpPacket.Property.Key) 21 | assert.Equal(t, true, sprpPacket.Property.Value.(bool)) 22 | assert.Equal(t, "ASYN_SPRP{ClockRef:11123bc18, Property:{ObeyEmptyMediaMarkers:%!s(bool=true)}}", sprpPacket.String()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_srat.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "math" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 9 | ) 10 | 11 | // AsynSratPacket is probably related to AVPlayer.SetRate somehow. I dont know exactly what everything means here 12 | type AsynSratPacket struct { 13 | ClockRef CFTypeID 14 | Rate1 float32 15 | Rate2 float32 16 | Time coremedia.CMTime 17 | } 18 | 19 | //NewAsynSratPacketFromBytes parses a new AsynSratPacket from bytes 20 | func NewAsynSratPacketFromBytes(data []byte) (AsynSratPacket, error) { 21 | var packet = AsynSratPacket{} 22 | remainingBytes, clockRef, err := ParseAsynHeader(data, SRAT) 23 | if err != nil { 24 | return packet, err 25 | } 26 | packet.ClockRef = clockRef 27 | 28 | packet.Rate1 = math.Float32frombits(binary.LittleEndian.Uint32(remainingBytes)) 29 | packet.Rate2 = math.Float32frombits(binary.LittleEndian.Uint32(remainingBytes[4:])) 30 | cmtime, err := coremedia.NewCMTimeFromBytes(remainingBytes[8:]) 31 | if err != nil { 32 | return packet, err 33 | } 34 | packet.Time = cmtime 35 | return packet, nil 36 | } 37 | 38 | func (sp AsynSratPacket) String() string { 39 | return fmt.Sprintf("ASYN_SRAT{ClockRef:%x, Rate1:%f, Rate2:%f, Time:%s}", sp.ClockRef, sp.Rate1, sp.Rate2, sp.Time.String()) 40 | } 41 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_srat_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestSrat(t *testing.T) { 13 | dat, err := ioutil.ReadFile("fixtures/asyn-srat") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | sratPacket, err := packet.NewAsynSratPacketFromBytes(dat) 18 | if assert.NoError(t, err) { 19 | assert.Equal(t, uint64(0x11123bc18), sratPacket.ClockRef) 20 | assert.Equal(t, float32(1), sratPacket.Rate1) 21 | assert.Equal(t, float32(1), sratPacket.Rate2) 22 | assert.Equal(t, uint32(1000000000), sratPacket.Time.CMTimeScale) 23 | assert.Equal(t, "ASYN_SRAT{ClockRef:11123bc18, Rate1:1.000000, Rate2:1.000000, Time:CMTime{1570648854000190667/1000000000, flags:KCMTimeFlagsHasBeenRounded, epoch:0}}", sratPacket.String()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_tbas.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | //AsynTbasPacket contains info about a new Timebase. I do not know what the other reference is used for. 9 | type AsynTbasPacket struct { 10 | ClockRef CFTypeID 11 | SomeOtherRef CFTypeID 12 | } 13 | 14 | //NewAsynTbasPacketFromBytes parses a AsynTbasPacket from bytes. 15 | func NewAsynTbasPacketFromBytes(data []byte) (AsynTbasPacket, error) { 16 | var packet = AsynTbasPacket{} 17 | remainingBytes, clockRef, err := ParseAsynHeader(data, TBAS) 18 | if err != nil { 19 | return packet, err 20 | } 21 | packet.ClockRef = clockRef 22 | packet.SomeOtherRef = binary.LittleEndian.Uint64(remainingBytes) 23 | return packet, nil 24 | } 25 | 26 | func (sp AsynTbasPacket) String() string { 27 | return fmt.Sprintf("ASYN_TBAS{ClockRef:%x, UnknownRef:%x}", sp.ClockRef, sp.SomeOtherRef) 28 | } 29 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_tbas_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestTbas(t *testing.T) { 13 | dat, err := ioutil.ReadFile("fixtures/asyn-tbas") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | tbasPacket, err := packet.NewAsynTbasPacketFromBytes(dat) 18 | if assert.NoError(t, err) { 19 | assert.Equal(t, uint64(0x11123bc18), tbasPacket.ClockRef) 20 | assert.Equal(t, uint64(0x1024490c0), tbasPacket.SomeOtherRef) 21 | assert.Equal(t, "ASYN_TBAS{ClockRef:11123bc18, UnknownRef:1024490c0}", tbasPacket.String()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type asynTestCase struct { 13 | actualBytes []byte 14 | expectedBytes []byte 15 | description string 16 | } 17 | 18 | func TestAsynPacket(t *testing.T) { 19 | cases := []asynTestCase{ 20 | { 21 | actualBytes: packet.AsynNeedPacketBytes(0x0000000102c16ca0), 22 | expectedBytes: loadFromFile("asyn-need"), 23 | description: "Expect Asyn Need to be correctly serialized", 24 | }, 25 | { 26 | actualBytes: packet.NewAsynHpd1Packet(packet.CreateHpd1DeviceInfoDict()), 27 | expectedBytes: loadFromFile("asyn-hpd1"), 28 | description: "Expect Asyn HPD1 to be correctly serialized", 29 | }, 30 | { 31 | actualBytes: packet.NewAsynHpa1Packet(packet.CreateHpa1DeviceInfoDict(), 0x00000001145392F0), 32 | expectedBytes: loadFromFile("asyn-hpa1"), 33 | description: "Expect Asyn HPA1 to be correctly serialized", 34 | }, 35 | { 36 | actualBytes: packet.NewAsynHPA0(0x0000000102C5FC10), 37 | expectedBytes: loadFromFile("asyn-hpa0"), 38 | description: "Expect Asyn HPA0 to be correctly serialized", 39 | }, 40 | { 41 | actualBytes: packet.NewAsynHPD0(), 42 | expectedBytes: loadFromFile("asyn-hpd0"), 43 | description: "Expect Asyn HPA0 to be correctly serialized", 44 | }, 45 | } 46 | for _, testCase := range cases { 47 | assert.Equal(t, testCase.expectedBytes, testCase.actualBytes, testCase.description) 48 | } 49 | } 50 | 51 | func loadFromFile(name string) []byte { 52 | dat, err := ioutil.ReadFile("fixtures/" + name) 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | return dat 57 | } 58 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_tjmp.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | //AsynTjmpPacket contains the data from a TJMP packet. 8 | //I think this is a notification sent by the device about changing a TimeBase. 9 | //I do not know what the last bytes are for currently. 10 | type AsynTjmpPacket struct { 11 | ClockRef CFTypeID 12 | Unknown []byte 13 | } 14 | 15 | //NewAsynTjmpPacketFromBytes parses a new AsynTjmpPacket from byte array 16 | func NewAsynTjmpPacketFromBytes(data []byte) (AsynTjmpPacket, error) { 17 | var packet = AsynTjmpPacket{} 18 | remainingBytes, clockRef, err := ParseAsynHeader(data, TJMP) 19 | if err != nil { 20 | return packet, err 21 | } 22 | packet.ClockRef = clockRef 23 | 24 | packet.Unknown = remainingBytes 25 | return packet, nil 26 | } 27 | 28 | func (sp AsynTjmpPacket) String() string { 29 | return fmt.Sprintf("ASYN_TJMP{ClockRef:%x, UnknownData:%x}", sp.ClockRef, sp.Unknown) 30 | } 31 | -------------------------------------------------------------------------------- /screencapture/packet/asyn_tjmp_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var expectedBytes = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} 13 | 14 | func TestTjmp(t *testing.T) { 15 | dat, err := ioutil.ReadFile("fixtures/asyn-tjmp") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | tjmpPacket, err := packet.NewAsynTjmpPacketFromBytes(dat) 20 | if assert.NoError(t, err) { 21 | assert.Equal(t, uint64(0x11123bc18), tjmpPacket.ClockRef) 22 | assert.Equal(t, expectedBytes, tjmpPacket.Unknown) 23 | assert.Equal(t, "ASYN_TJMP{ClockRef:11123bc18, UnknownData:0000000000000000000000000000000001000000010000000000000000000000000000000000000001000000010000000000000000000000}", tjmpPacket.String()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /screencapture/packet/fixtures/afmt-reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/afmt-reply -------------------------------------------------------------------------------- /screencapture/packet/fixtures/afmt-request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/afmt-request -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-eat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-eat -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-eat-nofdsc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-eat-nofdsc -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-feed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-feed -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-feed-nofdsc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-feed-nofdsc -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-feed-ttas-only: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-feed-ttas-only -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-feed-unknown1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-feed-unknown1 -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-hpa0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-hpa0 -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-hpa1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-hpa1 -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-hpd0: -------------------------------------------------------------------------------- 1 | nysa0dph -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-hpd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-hpd1 -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-need: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-need -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-rels: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-rels -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-sprp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-sprp -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-sprp2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-sprp2 -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-srat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-srat -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-tbas: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-tbas -------------------------------------------------------------------------------- /screencapture/packet/fixtures/asyn-tjmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/asyn-tjmp -------------------------------------------------------------------------------- /screencapture/packet/fixtures/clok-reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/clok-reply -------------------------------------------------------------------------------- /screencapture/packet/fixtures/clok-request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/clok-request -------------------------------------------------------------------------------- /screencapture/packet/fixtures/cvrp-reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/cvrp-reply -------------------------------------------------------------------------------- /screencapture/packet/fixtures/cvrp-request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/cvrp-request -------------------------------------------------------------------------------- /screencapture/packet/fixtures/cwpa-reply1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/cwpa-reply1 -------------------------------------------------------------------------------- /screencapture/packet/fixtures/cwpa-reply2: -------------------------------------------------------------------------------- 1 | ylpr@^QF෢ -------------------------------------------------------------------------------- /screencapture/packet/fixtures/cwpa-request1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/cwpa-request1 -------------------------------------------------------------------------------- /screencapture/packet/fixtures/cwpa-request2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/cwpa-request2 -------------------------------------------------------------------------------- /screencapture/packet/fixtures/og-reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/og-reply -------------------------------------------------------------------------------- /screencapture/packet/fixtures/og-request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/og-request -------------------------------------------------------------------------------- /screencapture/packet/fixtures/skew-reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/skew-reply -------------------------------------------------------------------------------- /screencapture/packet/fixtures/skew-request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/skew-request -------------------------------------------------------------------------------- /screencapture/packet/fixtures/stop-reply: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/stop-reply -------------------------------------------------------------------------------- /screencapture/packet/fixtures/stop-request: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/stop-request -------------------------------------------------------------------------------- /screencapture/packet/fixtures/time-reply1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/time-reply1 -------------------------------------------------------------------------------- /screencapture/packet/fixtures/time-request1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpaulus/quicktime_video_hack/d81396e2e7758d98c2a594853b64f98b54a8a871/screencapture/packet/fixtures/time-request1 -------------------------------------------------------------------------------- /screencapture/packet/ping.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import "encoding/binary" 4 | 5 | //Constants for creating a Ping packet 6 | const ( 7 | PingPacketMagic uint32 = 0x70696E67 8 | PingLength uint32 = 16 9 | PingHeader uint64 = 0x0000000100000000 10 | ) 11 | 12 | //NewPingPacketAsBytes generates a new default Ping packet 13 | func NewPingPacketAsBytes() []byte { 14 | packetBytes := make([]byte, 16) 15 | binary.LittleEndian.PutUint32(packetBytes, PingLength) 16 | binary.LittleEndian.PutUint32(packetBytes[4:], PingPacketMagic) 17 | binary.LittleEndian.PutUint64(packetBytes[8:], PingHeader) 18 | return packetBytes 19 | } 20 | -------------------------------------------------------------------------------- /screencapture/packet/ping_test.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "fmt" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | const pingPacketHexDump = "10000000676e69700000000001000000" 10 | 11 | func TestPingSerialization(t *testing.T) { 12 | assert.Equal(t, pingPacketHexDump, fmt.Sprintf("%x", NewPingPacketAsBytes())) 13 | } 14 | -------------------------------------------------------------------------------- /screencapture/packet/sync.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | ) 6 | 7 | //Different Sync Packet Magic Markers 8 | const ( 9 | SyncPacketMagic uint32 = 0x73796E63 10 | ReplyPacketMagic uint32 = 0x72706C79 11 | TIME uint32 = 0x74696D65 12 | CWPA uint32 = 0x63777061 13 | AFMT uint32 = 0x61666D74 14 | CVRP uint32 = 0x63767270 15 | CLOK uint32 = 0x636C6F6B 16 | OG uint32 = 0x676F2120 17 | SKEW uint32 = 0x736B6577 18 | STOP uint32 = 0x73746F70 19 | ) 20 | 21 | //CFTypeID is just a type alias for uint64 but I think it is closer to what is happening on MAC/iOS 22 | type CFTypeID = uint64 23 | 24 | //EmptyCFType is a CFTypeId of 0x1 25 | const EmptyCFType CFTypeID = 1 26 | 27 | func clockRefReply(clockRef uint64, correlationID uint64) []byte { 28 | length := 28 29 | data := make([]byte, length) 30 | binary.LittleEndian.PutUint32(data, uint32(length)) 31 | binary.LittleEndian.PutUint32(data[4:], ReplyPacketMagic) 32 | binary.LittleEndian.PutUint64(data[8:], correlationID) 33 | binary.LittleEndian.PutUint32(data[16:], 0) 34 | binary.LittleEndian.PutUint64(data[20:], clockRef) 35 | return data 36 | } 37 | -------------------------------------------------------------------------------- /screencapture/packet/sync_afmt.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | 7 | "github.com/danielpaulus/quicktime_video_hack/screencapture/common" 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 9 | ) 10 | 11 | // SyncAfmtPacket contains what I think is information about the audio format 12 | type SyncAfmtPacket struct { 13 | ClockRef CFTypeID 14 | CorrelationID uint64 15 | AudioStreamBasicDescription coremedia.AudioStreamBasicDescription 16 | } 17 | 18 | func (sp SyncAfmtPacket) String() string { 19 | return fmt.Sprintf("SYNC_AFMT{ClockRef:%x, CorrelationID:%x, AudioStreamBasicDescription:%s}", 20 | sp.ClockRef, sp.CorrelationID, sp.AudioStreamBasicDescription.String()) 21 | } 22 | 23 | // NewSyncAfmtPacketFromBytes parses a new AsynFmtPacket from byte array 24 | func NewSyncAfmtPacketFromBytes(data []byte) (SyncAfmtPacket, error) { 25 | remainingBytes, clockRef, correlationID, err := ParseSyncHeader(data, AFMT) 26 | if err != nil { 27 | return SyncAfmtPacket{}, err 28 | } 29 | packet := SyncAfmtPacket{ClockRef: clockRef, CorrelationID: correlationID} 30 | 31 | packet.AudioStreamBasicDescription, err = coremedia.NewAudioStreamBasicDescriptionFromBytes(remainingBytes) 32 | if err != nil { 33 | return packet, fmt.Errorf("Error parsing AudioStreamBasicDescription data in asyn afmt: %s, ", err) 34 | } 35 | return packet, nil 36 | } 37 | 38 | //NewReply returns a []byte containing a correct reploy for afmt 39 | func (sp SyncAfmtPacket) NewReply() []byte { 40 | responseDict := createResponseDict() 41 | dictBytes := coremedia.SerializeStringKeyDict(responseDict) 42 | dictLength := uint32(len(dictBytes)) 43 | length := dictLength + 20 44 | responseBytes := make([]byte, length) 45 | binary.LittleEndian.PutUint32(responseBytes, length) 46 | binary.LittleEndian.PutUint32(responseBytes[4:], ReplyPacketMagic) 47 | binary.LittleEndian.PutUint64(responseBytes[8:], sp.CorrelationID) 48 | binary.LittleEndian.PutUint32(responseBytes[16:], 0) 49 | 50 | copy(responseBytes[20:], dictBytes) 51 | return responseBytes 52 | 53 | } 54 | 55 | func createResponseDict() coremedia.StringKeyDict { 56 | var response coremedia.StringKeyDict 57 | errorCode := common.NewNSNumberFromUInt32(0) 58 | key := "Error" 59 | response = coremedia.StringKeyDict{Entries: make([]coremedia.StringKeyEntry, 1)} 60 | response.Entries[0].Key = key 61 | response.Entries[0].Value = errorCode 62 | return response 63 | } 64 | -------------------------------------------------------------------------------- /screencapture/packet/sync_afmt_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 9 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestAfmt(t *testing.T) { 14 | dat, err := ioutil.ReadFile("fixtures/afmt-request") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | afmtPacket, err := packet.NewSyncAfmtPacketFromBytes(dat[4:]) 19 | if assert.NoError(t, err) { 20 | assert.Equal(t, uint64(0x7fa66ce20cb0), afmtPacket.ClockRef) 21 | expectedAsbd := coremedia.DefaultAudioStreamBasicDescription() 22 | expectedAsbd.FormatFlags = 0x4C 23 | assert.Equal(t, expectedAsbd, afmtPacket.AudioStreamBasicDescription) 24 | expectedString := "SYNC_AFMT{ClockRef:7fa66ce20cb0, CorrelationID:113229d80, AudioStreamBasicDescription:{SampleRate:48000.000000,FormatFlags:76,BytesPerPacket:4,FramesPerPacket:1,BytesPerFrame:4,ChannelsPerFrame:2,BitsPerChannel:16,Reserved:0}}" 25 | assert.Equal(t, expectedString, afmtPacket.String()) 26 | testSerializationOfAfmtReply(afmtPacket, t) 27 | } 28 | 29 | _, err = packet.NewSyncAfmtPacketFromBytes(dat) 30 | assert.Error(t, err) 31 | } 32 | 33 | func testSerializationOfAfmtReply(clok packet.SyncAfmtPacket, t *testing.T) { 34 | replyBytes := clok.NewReply() 35 | expectedReplyBytes, err := ioutil.ReadFile("fixtures/afmt-reply") 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | assert.Equal(t, expectedReplyBytes, replyBytes) 40 | } 41 | -------------------------------------------------------------------------------- /screencapture/packet/sync_clok.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | //SyncClokPacket contains a decoded Clok packet from the device 8 | type SyncClokPacket struct { 9 | ClockRef CFTypeID 10 | CorrelationID uint64 11 | } 12 | 13 | //NewSyncClokPacketFromBytes parses a SynClokPacket from bytes 14 | func NewSyncClokPacketFromBytes(data []byte) (SyncClokPacket, error) { 15 | _, clockRef, correlationID, err := ParseSyncHeader(data, CLOK) 16 | if err != nil { 17 | return SyncClokPacket{}, err 18 | } 19 | packet := SyncClokPacket{ClockRef: clockRef, CorrelationID: correlationID} 20 | return packet, nil 21 | } 22 | 23 | //NewReply creates a RPLY message containing the given clockRef and serializes it into a []byte 24 | func (sp SyncClokPacket) NewReply(clockRef CFTypeID) []byte { 25 | return clockRefReply(clockRef, sp.CorrelationID) 26 | } 27 | 28 | func (sp SyncClokPacket) String() string { 29 | return fmt.Sprintf("SYNC_CLOK{ClockRef:%x, CorrelationID:%x}", sp.ClockRef, sp.CorrelationID) 30 | } 31 | -------------------------------------------------------------------------------- /screencapture/packet/sync_clok_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestClok(t *testing.T) { 13 | dat, err := ioutil.ReadFile("fixtures/clok-request") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | clok, err := packet.NewSyncClokPacketFromBytes(dat[4:]) 18 | if assert.NoError(t, err) { 19 | assert.Equal(t, uint64(0x7fa66cd10250), clok.ClockRef) 20 | assert.Equal(t, uint64(0x113584970), clok.CorrelationID) 21 | assert.Equal(t, "SYNC_CLOK{ClockRef:7fa66cd10250, CorrelationID:113584970}", clok.String()) 22 | } 23 | testSerializationOfClokReply(clok, t) 24 | _, err = packet.NewSyncClokPacketFromBytes(dat) 25 | assert.Error(t, err) 26 | } 27 | 28 | func testSerializationOfClokReply(clok packet.SyncClokPacket, t *testing.T) { 29 | var clockRef packet.CFTypeID = 0x00007FA67CC17980 30 | replyBytes := clok.NewReply(clockRef) 31 | expectedReplyBytes, err := ioutil.ReadFile("fixtures/clok-reply") 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | assert.Equal(t, expectedReplyBytes, replyBytes) 36 | } 37 | -------------------------------------------------------------------------------- /screencapture/packet/sync_cvrp.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | 7 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 8 | ) 9 | 10 | //SyncCvrpPacket contains all info from a CVRP packet sent by the device 11 | type SyncCvrpPacket struct { 12 | ClockRef CFTypeID 13 | CorrelationID uint64 14 | DeviceClockRef CFTypeID 15 | Payload coremedia.StringKeyDict 16 | } 17 | 18 | //NewSyncCvrpPacketFromBytes parses a SyncCvrpPacket from a []byte 19 | func NewSyncCvrpPacketFromBytes(data []byte) (SyncCvrpPacket, error) { 20 | remainingBytes, clockRef, correlationID, err := ParseSyncHeader(data, CVRP) 21 | if err != nil { 22 | return SyncCvrpPacket{}, err 23 | } 24 | packet := SyncCvrpPacket{ClockRef: clockRef, CorrelationID: correlationID} 25 | packet.ClockRef = binary.LittleEndian.Uint64(data[4:]) 26 | if packet.ClockRef != EmptyCFType { 27 | return packet, fmt.Errorf("CVRP packet should have empty CFTypeID for ClockRef but has:%x", packet.ClockRef) 28 | } 29 | 30 | packet.DeviceClockRef = binary.LittleEndian.Uint64(remainingBytes) 31 | 32 | payloadDict, err := coremedia.NewStringDictFromBytes(remainingBytes[8:]) 33 | if err != nil { 34 | return packet, err 35 | } 36 | packet.Payload = payloadDict 37 | return packet, nil 38 | } 39 | 40 | //NewReply creates a RPLY packet containing the given clockRef and serializes it to a []byte 41 | func (sp SyncCvrpPacket) NewReply(clockRef CFTypeID) []byte { 42 | return clockRefReply(clockRef, sp.CorrelationID) 43 | } 44 | 45 | func (sp SyncCvrpPacket) String() string { 46 | return fmt.Sprintf("SYNC_CVRP{ClockRef:%x, CorrelationID:%x, DeviceClockRef:%x, Payload:%s}", sp.ClockRef, sp.CorrelationID, sp.DeviceClockRef, sp.Payload.String()) 47 | } 48 | -------------------------------------------------------------------------------- /screencapture/packet/sync_cvrp_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const expectedCvrpString = "SYNC_CVRP{ClockRef:1, CorrelationID:1135659d0, DeviceClockRef:113538da0, Payload:StringKeyDict:[{PreparedQueueHighWaterLevel : StringKeyDict:[{flags : Int32[1]},{value : UInt64[5]},{timescale : Int32[30]},{epoch : UInt64[0]},]},{PreparedQueueLowWaterLevel : StringKeyDict:[{flags : Int32[1]},{value : UInt64[3]},{timescale : Int32[30]},{epoch : UInt64[0]},]},{FormatDescription : fdsc:{MediaType:Video, VideoDimension:(1126x2436), Codec:AVC-1, PPS:27640033ac5680470133e69e6e04040404, SPS:28ee3cb0, Extensions:IndexKeyDict:[{49 : IndexKeyDict:[{105 : 0x01640033ffe1001127640033ac5680470133e69e6e0404040401000428ee3cb0fdf8f800},]},{52 : H.264},]}},]}" 13 | 14 | func TestCvrp(t *testing.T) { 15 | dat, err := ioutil.ReadFile("fixtures/cvrp-request") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | cvrp, err := packet.NewSyncCvrpPacketFromBytes(dat[4:]) 20 | if assert.NoError(t, err) { 21 | assert.Equal(t, 3, len(cvrp.Payload.Entries)) 22 | assert.Equal(t, packet.EmptyCFType, cvrp.ClockRef) 23 | assert.Equal(t, uint64(0x113538da0), cvrp.DeviceClockRef) 24 | assert.Equal(t, uint64(0x1135659d0), cvrp.CorrelationID) 25 | assert.Equal(t, expectedCvrpString, cvrp.String()) 26 | } 27 | testSerializationOfCvrpReply(cvrp, t) 28 | _, err = packet.NewSyncCvrpPacketFromBytes(dat) 29 | assert.Error(t, err) 30 | } 31 | 32 | func testSerializationOfCvrpReply(cvrp packet.SyncCvrpPacket, t *testing.T) { 33 | var clockRef packet.CFTypeID = 0x00007FA66CD10250 34 | replyBytes := cvrp.NewReply(clockRef) 35 | expectedReplyBytes, err := ioutil.ReadFile("fixtures/cvrp-reply") 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | assert.Equal(t, expectedReplyBytes, replyBytes) 40 | } 41 | -------------------------------------------------------------------------------- /screencapture/packet/sync_cwpa.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | //SyncCwpaPacket contains all info from a CWPA packet sent by the device 9 | type SyncCwpaPacket struct { 10 | ClockRef CFTypeID 11 | CorrelationID uint64 12 | DeviceClockRef CFTypeID 13 | } 14 | 15 | //NewSyncCwpaPacketFromBytes parses a SyncCwpaPacket from a []byte 16 | func NewSyncCwpaPacketFromBytes(data []byte) (SyncCwpaPacket, error) { 17 | remainingBytes, clockRef, correlationID, err := ParseSyncHeader(data, CWPA) 18 | if err != nil { 19 | return SyncCwpaPacket{}, err 20 | } 21 | packet := SyncCwpaPacket{ClockRef: clockRef, CorrelationID: correlationID} 22 | packet.ClockRef = binary.LittleEndian.Uint64(data[4:]) 23 | if packet.ClockRef != EmptyCFType { 24 | return packet, fmt.Errorf("CWPA packet should have empty CFTypeID for ClockRef but has:%x", packet.ClockRef) 25 | } 26 | 27 | packet.DeviceClockRef = binary.LittleEndian.Uint64(remainingBytes) 28 | return packet, nil 29 | } 30 | 31 | //NewReply creates a RPLY packet containing the given clockRef and serializes it to a []byte 32 | func (sp SyncCwpaPacket) NewReply(clockRef CFTypeID) []byte { 33 | return clockRefReply(clockRef, sp.CorrelationID) 34 | } 35 | 36 | func (sp SyncCwpaPacket) String() string { 37 | return fmt.Sprintf("SYNC_CWPA{ClockRef:%x, CorrelationID:%x, DeviceClockRef:%x}", sp.ClockRef, sp.CorrelationID, sp.DeviceClockRef) 38 | } 39 | -------------------------------------------------------------------------------- /screencapture/packet/sync_cwpa_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestCwpa(t *testing.T) { 13 | dat, err := ioutil.ReadFile("fixtures/cwpa-request1") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | cwpa, err := packet.NewSyncCwpaPacketFromBytes(dat[4:]) 18 | if assert.NoError(t, err) { 19 | assert.Equal(t, packet.EmptyCFType, cwpa.ClockRef) 20 | assert.Equal(t, uint64(0x1135a74e0), cwpa.DeviceClockRef) 21 | assert.Equal(t, uint64(0x113573de0), cwpa.CorrelationID) 22 | assert.Equal(t, "SYNC_CWPA{ClockRef:1, CorrelationID:113573de0, DeviceClockRef:1135a74e0}", cwpa.String()) 23 | } 24 | _, err = packet.NewSyncCwpaPacketFromBytes(dat) 25 | assert.Error(t, err) 26 | 27 | brokenMessage := make([]byte, len(dat)) 28 | copy(brokenMessage, dat) 29 | testIncorrectClockRefProducesError(brokenMessage, t) 30 | 31 | copy(brokenMessage, dat) 32 | testIncorrectSubtypeProducesError(brokenMessage, t, cwpa) 33 | testSerializationOfReply(cwpa, t) 34 | } 35 | 36 | func testIncorrectClockRefProducesError(brokenMessage []byte, t *testing.T) { 37 | brokenMessage[9] = 0xFF 38 | _, err := packet.NewSyncCwpaPacketFromBytes(brokenMessage[4:]) 39 | assert.Error(t, err) 40 | } 41 | 42 | func testIncorrectSubtypeProducesError(brokenMessage []byte, t *testing.T, cwpa packet.SyncCwpaPacket) { 43 | brokenMessage[17] = 0xFF 44 | _, err := packet.NewSyncCwpaPacketFromBytes(brokenMessage[4:]) 45 | assert.Error(t, err) 46 | } 47 | 48 | func testSerializationOfReply(cwpa packet.SyncCwpaPacket, t *testing.T) { 49 | var clockRef packet.CFTypeID = 0x00007FA66CE20CB0 50 | replyBytes := cwpa.NewReply(clockRef) 51 | expectedReplyBytes, err := ioutil.ReadFile("fixtures/cwpa-reply1") 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | assert.Equal(t, expectedReplyBytes, replyBytes) 56 | } 57 | -------------------------------------------------------------------------------- /screencapture/packet/sync_og.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | //SyncOgPacket represents the OG Message. I do not know what these messages mean. 9 | type SyncOgPacket struct { 10 | ClockRef CFTypeID 11 | CorrelationID uint64 12 | Unknown uint32 13 | } 14 | 15 | //NewSyncOgPacketFromBytes parses a SyncOgPacket form bytes assuming it starts with SYNC magic and has the correct length. 16 | func NewSyncOgPacketFromBytes(data []byte) (SyncOgPacket, error) { 17 | remainingBytes, clockRef, correlationID, err := ParseSyncHeader(data, OG) 18 | if err != nil { 19 | return SyncOgPacket{}, err 20 | } 21 | packet := SyncOgPacket{ClockRef: clockRef, CorrelationID: correlationID} 22 | 23 | packet.Unknown = binary.LittleEndian.Uint32(remainingBytes) 24 | return packet, nil 25 | } 26 | 27 | //NewReply returns a []byte containing the default reply for a SyncOgPacket 28 | func (sp SyncOgPacket) NewReply() []byte { 29 | responseBytes := make([]byte, 24) 30 | binary.LittleEndian.PutUint32(responseBytes, 24) 31 | binary.LittleEndian.PutUint32(responseBytes[4:], ReplyPacketMagic) 32 | binary.LittleEndian.PutUint64(responseBytes[8:], sp.CorrelationID) 33 | binary.LittleEndian.PutUint64(responseBytes[16:], 0) 34 | 35 | return responseBytes 36 | 37 | } 38 | 39 | func (sp SyncOgPacket) String() string { 40 | return fmt.Sprintf("SYNC_OG{ClockRef:%x, CorrelationID:%x, Unknown:%d}", sp.ClockRef, sp.CorrelationID, sp.Unknown) 41 | } 42 | -------------------------------------------------------------------------------- /screencapture/packet/sync_og_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestOg(t *testing.T) { 13 | dat, err := ioutil.ReadFile("fixtures/og-request") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | og, err := packet.NewSyncOgPacketFromBytes(dat[4:]) 18 | if assert.NoError(t, err) { 19 | assert.Equal(t, uint64(0x7fba35425ff0), og.ClockRef) 20 | assert.Equal(t, uint64(0x102d32f30), og.CorrelationID) 21 | assert.Equal(t, "SYNC_OG{ClockRef:7fba35425ff0, CorrelationID:102d32f30, Unknown:1}", og.String()) 22 | } 23 | testSerializationOfOgReply(og, t) 24 | _, err = packet.NewSyncOgPacketFromBytes(dat) 25 | assert.Error(t, err) 26 | } 27 | 28 | func testSerializationOfOgReply(clok packet.SyncOgPacket, t *testing.T) { 29 | replyBytes := clok.NewReply() 30 | expectedReplyBytes, err := ioutil.ReadFile("fixtures/og-reply") 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | assert.Equal(t, expectedReplyBytes, replyBytes) 35 | } 36 | -------------------------------------------------------------------------------- /screencapture/packet/sync_skew.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "math" 7 | ) 8 | 9 | //SyncSkewPacket requests us to reply with the current skew value 10 | type SyncSkewPacket struct { 11 | ClockRef CFTypeID 12 | CorrelationID uint64 13 | } 14 | 15 | //NewSyncSkewPacketFromBytes parses a SyncSkewPacket from bytes 16 | func NewSyncSkewPacketFromBytes(data []byte) (SyncSkewPacket, error) { 17 | _, clockRef, correlationID, err := ParseSyncHeader(data, SKEW) 18 | if err != nil { 19 | return SyncSkewPacket{}, err 20 | } 21 | packet := SyncSkewPacket{ClockRef: clockRef, CorrelationID: correlationID} 22 | return packet, nil 23 | } 24 | 25 | //NewReply creates a byte array containing the given skew 26 | func (sp SyncSkewPacket) NewReply(skew float64) []byte { 27 | responseBytes := make([]byte, 28) 28 | binary.LittleEndian.PutUint32(responseBytes, 28) 29 | binary.LittleEndian.PutUint32(responseBytes[4:], ReplyPacketMagic) 30 | binary.LittleEndian.PutUint64(responseBytes[8:], sp.CorrelationID) 31 | binary.LittleEndian.PutUint32(responseBytes[16:], 0) 32 | binary.LittleEndian.PutUint64(responseBytes[20:], math.Float64bits(skew)) 33 | return responseBytes 34 | } 35 | 36 | func (sp SyncSkewPacket) String() string { 37 | return fmt.Sprintf("SYNC_SKEW{ClockRef:%x, CorrelationID:%x}", sp.ClockRef, sp.CorrelationID) 38 | } 39 | -------------------------------------------------------------------------------- /screencapture/packet/sync_skew_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestSkew(t *testing.T) { 13 | dat, err := ioutil.ReadFile("fixtures/skew-request") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | skew, err := packet.NewSyncSkewPacketFromBytes(dat[4:]) 18 | if assert.NoError(t, err) { 19 | assert.Equal(t, uint64(0x7fba35425ff0), skew.ClockRef) 20 | assert.Equal(t, uint64(0x102fdb960), skew.CorrelationID) 21 | assert.Equal(t, "SYNC_SKEW{ClockRef:7fba35425ff0, CorrelationID:102fdb960}", skew.String()) 22 | } 23 | testSerializationOfSkewReply(skew, t) 24 | _, err = packet.NewSyncSkewPacketFromBytes(dat) 25 | assert.Error(t, err) 26 | 27 | } 28 | 29 | func testSerializationOfSkewReply(skew packet.SyncSkewPacket, t *testing.T) { 30 | replyBytes := skew.NewReply(float64(48000)) 31 | expectedReplyBytes, err := ioutil.ReadFile("fixtures/skew-reply") 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | assert.Equal(t, expectedReplyBytes, replyBytes) 36 | } 37 | -------------------------------------------------------------------------------- /screencapture/packet/sync_stop.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | //SyncStopPacket requests us to stop our clock 9 | type SyncStopPacket struct { 10 | ClockRef CFTypeID 11 | CorrelationID uint64 12 | } 13 | 14 | //NewSyncStopPacketFromBytes parses a SyncStopPacket from bytes 15 | func NewSyncStopPacketFromBytes(data []byte) (SyncStopPacket, error) { 16 | _, clockRef, correlationID, err := ParseSyncHeader(data, STOP) 17 | if err != nil { 18 | return SyncStopPacket{}, err 19 | } 20 | packet := SyncStopPacket{ClockRef: clockRef, CorrelationID: correlationID} 21 | return packet, nil 22 | } 23 | 24 | //NewReply creates a byte array containing the given skew 25 | func (sp SyncStopPacket) NewReply() []byte { 26 | responseBytes := make([]byte, 24) 27 | binary.LittleEndian.PutUint32(responseBytes, 24) 28 | binary.LittleEndian.PutUint32(responseBytes[4:], ReplyPacketMagic) 29 | binary.LittleEndian.PutUint64(responseBytes[8:], sp.CorrelationID) 30 | binary.LittleEndian.PutUint32(responseBytes[16:], 0) 31 | binary.LittleEndian.PutUint32(responseBytes[20:], 0) 32 | return responseBytes 33 | } 34 | 35 | func (sp SyncStopPacket) String() string { 36 | return fmt.Sprintf("SYNC_STOP{ClockRef:%x, CorrelationID:%x}", sp.ClockRef, sp.CorrelationID) 37 | } 38 | -------------------------------------------------------------------------------- /screencapture/packet/sync_stop_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestStop(t *testing.T) { 13 | dat, err := ioutil.ReadFile("fixtures/stop-request") 14 | if err != nil { 15 | log.Fatal(err) 16 | } 17 | stop, err := packet.NewSyncStopPacketFromBytes(dat[4:]) 18 | if assert.NoError(t, err) { 19 | assert.Equal(t, uint64(0x7fba35425ff0), stop.ClockRef) 20 | assert.Equal(t, uint64(0x102fd4910), stop.CorrelationID) 21 | assert.Equal(t, "SYNC_STOP{ClockRef:7fba35425ff0, CorrelationID:102fd4910}", stop.String()) 22 | } 23 | testSerializationOfStopReply(stop, t) 24 | _, err = packet.NewSyncStopPacketFromBytes(dat) 25 | assert.Error(t, err) 26 | 27 | } 28 | 29 | func testSerializationOfStopReply(stop packet.SyncStopPacket, t *testing.T) { 30 | replyBytes := stop.NewReply() 31 | expectedReplyBytes, err := ioutil.ReadFile("fixtures/stop-reply") 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | assert.Equal(t, expectedReplyBytes, replyBytes) 36 | } 37 | -------------------------------------------------------------------------------- /screencapture/packet/sync_time.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | 7 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 8 | ) 9 | 10 | //SyncTimePacket contains the data from a decoded Time Packet sent by the device 11 | type SyncTimePacket struct { 12 | ClockRef CFTypeID 13 | CorrelationID uint64 14 | } 15 | 16 | //NewSyncTimePacketFromBytes parses a SyncTimePacket from bytes 17 | func NewSyncTimePacketFromBytes(data []byte) (SyncTimePacket, error) { 18 | _, clockRef, correlationID, err := ParseSyncHeader(data, TIME) 19 | if err != nil { 20 | return SyncTimePacket{}, err 21 | } 22 | packet := SyncTimePacket{ClockRef: clockRef, CorrelationID: correlationID} 23 | return packet, nil 24 | } 25 | 26 | //NewReply creates a RPLY packet containing the given CMTime and serializes it to a []byte 27 | func (sp SyncTimePacket) NewReply(time coremedia.CMTime) ([]byte, error) { 28 | length := 44 29 | data := make([]byte, length) 30 | binary.LittleEndian.PutUint32(data, uint32(length)) 31 | binary.LittleEndian.PutUint32(data[4:], ReplyPacketMagic) 32 | binary.LittleEndian.PutUint64(data[8:], sp.CorrelationID) 33 | binary.LittleEndian.PutUint32(data[16:], 0) 34 | err := time.Serialize(data[20:]) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return data, nil 39 | } 40 | 41 | func (sp SyncTimePacket) String() string { 42 | return fmt.Sprintf("SYNC_TIME{ClockRef:%x, CorrelationID:%x}", sp.ClockRef, sp.CorrelationID) 43 | } 44 | -------------------------------------------------------------------------------- /screencapture/packet/sync_time_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "testing" 7 | 8 | "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia" 9 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestTime(t *testing.T) { 14 | dat, err := ioutil.ReadFile("fixtures/time-request1") 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | timePacket, err := packet.NewSyncTimePacketFromBytes(dat[4:]) 19 | if assert.NoError(t, err) { 20 | assert.Equal(t, uint64(0x7fa67cc17980), timePacket.ClockRef) 21 | assert.Equal(t, uint64(0x113223d50), timePacket.CorrelationID) 22 | assert.Equal(t, "SYNC_TIME{ClockRef:7fa67cc17980, CorrelationID:113223d50}", timePacket.String()) 23 | } 24 | testSerializationOfTimeReply(timePacket, t) 25 | _, err = packet.NewSyncTimePacketFromBytes(dat) 26 | assert.Error(t, err) 27 | } 28 | 29 | func testSerializationOfTimeReply(timePacket packet.SyncTimePacket, t *testing.T) { 30 | cmtime := coremedia.CMTime{ 31 | CMTimeValue: 0x0000BA62C442E1E1, 32 | CMTimeScale: 0x3B9ACA00, 33 | CMTimeFlags: coremedia.KCMTimeFlagsHasBeenRounded, 34 | CMTimeEpoch: 0, 35 | } 36 | replyBytes, err := timePacket.NewReply(cmtime) 37 | if assert.NoError(t, err) { 38 | expectedReplyBytes, err := ioutil.ReadFile("fixtures/time-reply1") 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | assert.Equal(t, expectedReplyBytes, replyBytes) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /screencapture/packet/util.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | //ParseAsynHeader checks for the ASYN magic and the given messagemagic and then returns 9 | //the remainingBytes starting after the messagemagic so after 16 bytes, the clockRef and an error 10 | //is the packet is not Asyn or if the messagemagic is wrong 11 | func ParseAsynHeader(data []byte, messagemagic uint32) ([]byte, CFTypeID, error) { 12 | return parseHeader(data, AsynPacketMagic, messagemagic) 13 | } 14 | 15 | //ParseSyncHeader checks for the SYNC magic and the given messagemagic and then returns 16 | //the remainingBytes starting after the messagemagic so after 16 bytes, the clockRef, correlationID and an error 17 | //is the packet is not SYNC or if the messagemagic is wrong 18 | func ParseSyncHeader(data []byte, messagemagic uint32) ([]byte, CFTypeID, uint64, error) { 19 | remainingBytes, clockRef, err := parseHeader(data, SyncPacketMagic, messagemagic) 20 | if err != nil { 21 | return data, 0, 0, err 22 | } 23 | correlationID := binary.LittleEndian.Uint64(remainingBytes) 24 | return remainingBytes[8:], clockRef, correlationID, err 25 | } 26 | 27 | func parseHeader(data []byte, packetmagic uint32, messagemagic uint32) ([]byte, CFTypeID, error) { 28 | magic := binary.LittleEndian.Uint32(data) 29 | if magic != packetmagic { 30 | packetTypeASCII := string(data[:4]) 31 | return nil, 0, fmt.Errorf("invalid packet magic '%s' - packethex: %x", packetTypeASCII, data) 32 | } 33 | clockRef := binary.LittleEndian.Uint64(data[4:]) 34 | messageType := binary.LittleEndian.Uint32(data[12:]) 35 | if messageType != messagemagic { 36 | messageTypeASCII := string(data[12:16]) 37 | return nil, 0, fmt.Errorf("invalid packet type:'%s' - packethex: %x", messageTypeASCII, data) 38 | } 39 | return data[16:], clockRef, nil 40 | } 41 | -------------------------------------------------------------------------------- /screencapture/packet/util_test.go: -------------------------------------------------------------------------------- 1 | package packet_test 2 | 3 | import ( 4 | "encoding/binary" 5 | "testing" 6 | 7 | "github.com/danielpaulus/quicktime_video_hack/screencapture/packet" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestParseAsynHeader(t *testing.T) { 12 | data, expectedBytes, expectedClockRef := createAsynFeedPacket() 13 | 14 | remainingBytes, clockRef, err := packet.ParseAsynHeader(data, packet.FEED) 15 | if assert.NoError(t, err) { 16 | assert.Equal(t, expectedClockRef, clockRef) 17 | assert.Equal(t, expectedBytes, remainingBytes) 18 | } 19 | 20 | _, _, err = packet.ParseAsynHeader(data, packet.TJMP) 21 | assert.Error(t, err) 22 | assert.Equal(t, "invalid packet type:'deef' - packethex: 6e79736100ccbbaa3312ff006465656601020304", err.Error()) 23 | //break asyn magic marker 24 | data[0] = 80 25 | _, _, err = packet.ParseAsynHeader(data, packet.FEED) 26 | assert.Error(t, err) 27 | assert.Equal(t, "invalid packet magic 'Pysa' - packethex: 5079736100ccbbaa3312ff006465656601020304", err.Error()) 28 | } 29 | 30 | func TestParseSyncHeader(t *testing.T) { 31 | data, expectedBytes, expectedClockRef, expectedCorrelationID := createSyncClokPacket() 32 | 33 | remainingBytes, clockRef, correlationID, err := packet.ParseSyncHeader(data, packet.FEED) 34 | if assert.NoError(t, err) { 35 | assert.Equal(t, expectedClockRef, clockRef) 36 | assert.Equal(t, expectedBytes, remainingBytes) 37 | assert.Equal(t, expectedCorrelationID, correlationID) 38 | } 39 | 40 | _, _, err = packet.ParseAsynHeader(data, packet.TJMP) 41 | assert.Error(t, err) 42 | 43 | //break sync magic marker 44 | data[0] = 80 45 | _, _, err = packet.ParseAsynHeader(data, packet.FEED) 46 | assert.Error(t, err) 47 | } 48 | 49 | func createAsynFeedPacket() ([]byte, []byte, packet.CFTypeID) { 50 | data := make([]byte, 20) 51 | binary.LittleEndian.PutUint32(data, packet.AsynPacketMagic) 52 | expectedClockRef := uint64(0xff1233aabbcc00) 53 | binary.LittleEndian.PutUint64(data[4:], expectedClockRef) 54 | binary.LittleEndian.PutUint32(data[12:], packet.FEED) 55 | expectedBytes := []byte{1, 2, 3, 4} 56 | copy(data[16:], expectedBytes) 57 | return data, expectedBytes, expectedClockRef 58 | } 59 | 60 | func createSyncClokPacket() ([]byte, []byte, packet.CFTypeID, uint64) { 61 | data := make([]byte, 28) 62 | binary.LittleEndian.PutUint32(data, packet.SyncPacketMagic) 63 | expectedClockRef := uint64(0xff1233aabbcc00) 64 | binary.LittleEndian.PutUint64(data[4:], expectedClockRef) 65 | binary.LittleEndian.PutUint32(data[12:], packet.FEED) 66 | expectedCorrelationID := uint64(0xFFDDFFAA) 67 | binary.LittleEndian.PutUint64(data[16:], expectedCorrelationID) 68 | expectedBytes := []byte{1, 2, 3, 4} 69 | copy(data[24:], expectedBytes) 70 | return data, expectedBytes, expectedClockRef, expectedCorrelationID 71 | } 72 | -------------------------------------------------------------------------------- /screencapture/usbadapter.go: -------------------------------------------------------------------------------- 1 | package screencapture 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/pkg/errors" 9 | 10 | "github.com/google/gousb" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | //UsbAdapter reads and writes from AV Quicktime USB Bulk endpoints 15 | type UsbAdapter struct { 16 | outEndpoint *gousb.OutEndpoint 17 | Dump bool 18 | DumpOutWriter io.Writer 19 | DumpInWriter io.Writer 20 | } 21 | 22 | //WriteDataToUsb implements the UsbWriter interface and sends the byte array to the usb bulk endpoint. 23 | func (usbAdapter *UsbAdapter) WriteDataToUsb(bytes []byte) { 24 | _, err := usbAdapter.outEndpoint.Write(bytes) 25 | if err != nil { 26 | log.Error("failed sending to usb", err) 27 | } 28 | if usbAdapter.Dump { 29 | _, err := usbAdapter.DumpOutWriter.Write(bytes) 30 | if err != nil { 31 | log.Fatalf("Failed dumping data:%v", err) 32 | } 33 | } 34 | } 35 | 36 | //StartReading claims the AV Quicktime USB Bulk endpoints and starts reading until a stopSignal is sent. 37 | //Every received data is added to a frameextractor and when it is complete, sent to the UsbDataReceiver. 38 | func (usbAdapter *UsbAdapter) StartReading(device IosDevice, receiver UsbDataReceiver, stopSignal chan interface{}) error { 39 | ctx, cleanUp := createContext() 40 | defer cleanUp() 41 | 42 | usbDevice, err := OpenDevice(ctx, device) 43 | if err != nil { 44 | return err 45 | } 46 | if !device.IsActivated() { 47 | return errors.New("device not activated for screen mirroring") 48 | } 49 | 50 | confignum, _ := usbDevice.ActiveConfigNum() 51 | log.Debugf("Config is active: %d, QT config is: %d", confignum, device.QTConfigIndex) 52 | 53 | config, err := usbDevice.Config(device.QTConfigIndex) 54 | if err != nil { 55 | return errors.New("Could not retrieve config") 56 | } 57 | 58 | log.Debugf("QT Config is active: %s", config.String()) 59 | 60 | iface, err := findAndClaimQuickTimeInterface(config) 61 | if err != nil { 62 | log.Debug("could not get Quicktime Interface") 63 | return err 64 | } 65 | log.Debugf("Got QT iface:%s", iface.String()) 66 | 67 | inboundBulkEndpointIndex, inboundBulkEndpointAddress, err := findBulkEndpoint(iface.Setting, gousb.EndpointDirectionIn) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | outboundBulkEndpointIndex, outboundBulkEndpointAddress, err := findBulkEndpoint(iface.Setting, gousb.EndpointDirectionOut) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | err = clearFeature(usbDevice, inboundBulkEndpointAddress, outboundBulkEndpointAddress) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | inEndpoint, err := iface.InEndpoint(inboundBulkEndpointIndex) 83 | if err != nil { 84 | log.Error("couldnt get InEndpoint") 85 | return err 86 | } 87 | log.Debugf("Inbound Bulk: %s", inEndpoint.String()) 88 | 89 | outEndpoint, err := iface.OutEndpoint(outboundBulkEndpointIndex) 90 | if err != nil { 91 | log.Error("couldnt get OutEndpoint") 92 | return err 93 | } 94 | log.Debugf("Outbound Bulk: %s", outEndpoint.String()) 95 | usbAdapter.outEndpoint = outEndpoint 96 | 97 | stream, err := inEndpoint.NewStream(4096, 5) 98 | if err != nil { 99 | log.Fatal("couldnt create stream") 100 | return err 101 | } 102 | log.Debug("Endpoint claimed") 103 | log.Infof("Device '%s' USB connection ready, waiting for ping..", device.SerialNumber) 104 | go func() { 105 | lengthBuffer := make([]byte, 4) 106 | for { 107 | n, err := io.ReadFull(stream, lengthBuffer) 108 | if err != nil { 109 | log.Errorf("Failed reading 4bytes length with err:%s only received: %d", err, n) 110 | return 111 | } 112 | //the 4 bytes header are included in the length, so we need to subtract them 113 | //here to know how long the payload will be 114 | length := binary.LittleEndian.Uint32(lengthBuffer) - 4 115 | dataBuffer := make([]byte, length) 116 | 117 | n, err = io.ReadFull(stream, dataBuffer) 118 | if err != nil { 119 | log.Errorf("Failed reading payload with err:%s only received: %d/%d bytes", err, n, length) 120 | var signal interface{} 121 | stopSignal <- signal 122 | return 123 | } 124 | if usbAdapter.Dump { 125 | _, err := usbAdapter.DumpInWriter.Write(dataBuffer) 126 | if err != nil { 127 | log.Fatalf("Failed dumping data:%v", err) 128 | } 129 | } 130 | receiver.ReceiveData(dataBuffer) 131 | } 132 | }() 133 | 134 | <-stopSignal 135 | receiver.CloseSession() 136 | log.Info("Closing usb stream") 137 | 138 | err = stream.Close() 139 | if err != nil { 140 | log.Error("Error closing stream", err) 141 | } 142 | log.Info("Closing usb interface") 143 | iface.Close() 144 | 145 | sendQTDisableConfigControlRequest(usbDevice) 146 | log.Debug("Resetting device config") 147 | _, err = usbDevice.Config(device.UsbMuxConfigIndex) 148 | if err != nil { 149 | log.Warn(err) 150 | } 151 | 152 | return nil 153 | } 154 | 155 | func clearFeature(usbDevice *gousb.Device, inboundBulkEndpointAddress gousb.EndpointAddress, outboundBulkEndpointAddress gousb.EndpointAddress) error { 156 | val, err := usbDevice.Control(0x02, 0x01, 0, uint16(inboundBulkEndpointAddress), make([]byte, 0)) 157 | if err != nil { 158 | return errors.Wrap(err, "clear feature failed") 159 | } 160 | log.Debugf("Clear Feature RC: %d", val) 161 | 162 | val, err = usbDevice.Control(0x02, 0x01, 0, uint16(outboundBulkEndpointAddress), make([]byte, 0)) 163 | log.Debugf("Clear Feature RC: %d", val) 164 | return errors.Wrap(err, "clear feature failed") 165 | } 166 | 167 | func findBulkEndpoint(setting gousb.InterfaceSetting, direction gousb.EndpointDirection) (int, gousb.EndpointAddress, error) { 168 | for _, v := range setting.Endpoints { 169 | if v.Direction == direction { 170 | return v.Number, v.Address, nil 171 | 172 | } 173 | } 174 | return 0, 0, errors.New("Inbound Bulkendpoint not found") 175 | } 176 | 177 | func findAndClaimQuickTimeInterface(config *gousb.Config) (*gousb.Interface, error) { 178 | log.Debug("Looking for quicktime interface..") 179 | found, ifaceIndex := findInterfaceForSubclass(config.Desc, QuicktimeSubclass) 180 | if !found { 181 | return nil, fmt.Errorf("did not find interface %v", config) 182 | } 183 | log.Debugf("Found Quicktimeinterface: %d", ifaceIndex) 184 | return config.Interface(ifaceIndex, 0) 185 | } 186 | --------------------------------------------------------------------------------