Skip to content

Commit b36acdc

Browse files
authored
Add support for custom gst pipelines (#40)
* add custom pipeline parsing * add custom pipeline support to CLI interface * add appsrces to pipeline, add example * add support for custom pipelines, add EOS signal support, add first example of mp4 pipeline (audio does not work here, will figure out why) * add constants to linux code * move fixes to linux code * move fixes to linux code * add working example for writing to mp4 file * add working mac os example * add hack to wait long enough for MOOV box to be written, add working linux example * wait for EOS signal instead of sleeping a second * add Stop to interface, increase timeout * increase timeout * fix test * bump version, update readme
1 parent 26f63bf commit b36acdc

File tree

10 files changed

+255
-16
lines changed

10 files changed

+255
-16
lines changed

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ Currently you can use it to dump a h264 file and a wave file or mirror your devi
2424

2525
## 3. Usage
2626
```
27-
Q.uickTime V.ideo H.ack (qvh) v0.2-beta
27+
Q.uickTime V.ideo H.ack (qvh) v0.3-beta
2828
2929
Usage:
3030
qvh devices [-v]
3131
qvh activate [--udid=<udid>] [-v]
3232
qvh record <h264file> <wavfile> [-v] [--udid=<udid>]
33-
qvh gstreamer [-v]
33+
qvh gstreamer [--pipeline=<pipeline>] [--examples] [-v]
3434
qvh --version | version
3535
3636
@@ -41,12 +41,15 @@ Options:
4141
--udid=<udid> UDID of the device. If not specified, the first found device will be used automatically.
4242
4343
The commands work as following:
44-
devices lists iOS devices attached to this host and tells you if video streaming was activated for them
45-
activate enables the video streaming config for the device specified by --udid
46-
record will start video&audio recording. Video will be saved in a raw h264 file playable by VLC.
47-
Audio will be saved in a uncompressed wav file.
48-
Run like: "qvh record /home/yourname/out.h264 /home/yourname/out.wav"
49-
gstreamer qvh will open a new window and push AV data to gstreamer.
44+
devices lists iOS devices attached to this host and tells you if video streaming was activated for them
45+
activate enables the video streaming config for the device specified by --udid
46+
record will start video&audio recording. Video will be saved in a raw h264 file playable by VLC.
47+
Audio will be saved in a uncompressed wav file. Run like: "qvh record /home/yourname/out.h264 /home/yourname/out.wav"
48+
49+
gstreamer If no additional param is provided, qvh will open a new window and push AV data to gstreamer.
50+
If "qvh gstreamer --examples" is provided, qvh will print some common gstreamer pipeline examples.
51+
If --pipeline is provided, qvh will use the provided gstreamer pipeline instead of
52+
displaying audio and video in a window.
5053
```
5154

5255
## 3. Technical Docs/ Roll your own implementation

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.13
44

55
require (
66
github.com/danielpaulus/go-ios v0.0.0-20190926190740-cc977db05eea
7-
github.com/danielpaulus/gst v0.0.0-20191031112308-dfcba780ec66
7+
github.com/danielpaulus/gst v0.0.0-20200201205042-e6d2974fceb8
88
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
99
github.com/google/gousb v0.0.0-20190812193832-18f4c1d8a750
1010
github.com/lijo-jose/glib v0.0.0-20191012030101-93ee72d7d646

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ github.com/danielpaulus/gst v0.0.0-20191031102428-eb100cb83442 h1:AKA48ahee1DRBV
44
github.com/danielpaulus/gst v0.0.0-20191031102428-eb100cb83442/go.mod h1:JbhjLST5AaUXpKQK65g9144BK8QHftbpuFoYuhDuONw=
55
github.com/danielpaulus/gst v0.0.0-20191031112308-dfcba780ec66 h1:UDoHlePd6+rNFfJ/s2ASd2MTRE0W1sWzCeujJVTjVKU=
66
github.com/danielpaulus/gst v0.0.0-20191031112308-dfcba780ec66/go.mod h1:JbhjLST5AaUXpKQK65g9144BK8QHftbpuFoYuhDuONw=
7+
github.com/danielpaulus/gst v0.0.0-20200201205042-e6d2974fceb8 h1:+XiTgRoo1bCA3paC4e/0WYWI7+J2O7hR/IYuSikANMw=
8+
github.com/danielpaulus/gst v0.0.0-20200201205042-e6d2974fceb8/go.mod h1:JbhjLST5AaUXpKQK65g9144BK8QHftbpuFoYuhDuONw=
79
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
810
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
911
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

main.go

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
log "github.com/sirupsen/logrus"
1515
)
1616

17-
const version = "v0.2-beta"
17+
const version = "v0.3-beta"
1818

1919
func main() {
2020
usage := fmt.Sprintf(`Q.uickTime V.ideo H.ack (qvh) %s
@@ -23,7 +23,7 @@ Usage:
2323
qvh devices [-v]
2424
qvh activate [--udid=<udid>] [-v]
2525
qvh record <h264file> <wavfile> [-v] [--udid=<udid>]
26-
qvh gstreamer [-v]
26+
qvh gstreamer [--pipeline=<pipeline>] [--examples] [-v]
2727
qvh --version | version
2828
2929
@@ -36,10 +36,13 @@ Options:
3636
The commands work as following:
3737
devices lists iOS devices attached to this host and tells you if video streaming was activated for them
3838
activate enables the video streaming config for the device specified by --udid
39-
record will start video&audio recording. Video will be saved in a raw h264 file playable by VLC.
40-
Audio will be saved in a uncompressed wav file.
41-
Run like: "qvh record /home/yourname/out.h264 /home/yourname/out.wav"
42-
gstreamer qvh will open a new window and push AV data to gstreamer.
39+
record will start video&audio recording. Video will be saved in a raw h264 file playable by VLC.
40+
Audio will be saved in a uncompressed wav file. Run like: "qvh record /home/yourname/out.h264 /home/yourname/out.wav"
41+
42+
gstreamer If no additional param is provided, qvh will open a new window and push AV data to gstreamer.
43+
If "qvh gstreamer --examples" is provided, qvh will print some common gstreamer pipeline examples.
44+
If --pipeline is provided, qvh will use the provided gstreamer pipeline instead of
45+
displaying audio and video in a window.
4346
`, version)
4447
arguments, _ := docopt.ParseDoc(usage)
4548
log.SetFormatter(&log.JSONFormatter{})
@@ -87,7 +90,17 @@ The commands work as following:
8790
}
8891
gstreamerCommand, _ := arguments.Bool("gstreamer")
8992
if gstreamerCommand {
90-
startGStreamer(udid)
93+
shouldPrintExamples, _ := arguments.Bool("--examples")
94+
if shouldPrintExamples {
95+
printExamples()
96+
return
97+
}
98+
gstPipeline, _ := arguments.String("--pipeline")
99+
if gstPipeline == "" {
100+
startGStreamer(udid)
101+
return
102+
}
103+
startGStreamerWithCustomPipeline(udid, gstPipeline)
91104
}
92105
}
93106

@@ -98,6 +111,42 @@ func printVersion() {
98111
printJSON(versionMap)
99112
}
100113

114+
func printExamples() {
115+
116+
examples := `Examples:
117+
118+
Writing an MP4 file
119+
This pipeline will save the recording in video.mp4 with h264 and aac format. The default settings
120+
of this pipeline will create a compressed video that takes up way less space than raw h264.
121+
Note that you need to set "ignore-length" on the wavparse because we are streaming and do not know the length in advance.
122+
123+
Write MP4 file Mac OSX:
124+
vtdec is the hardware accelerated decoder on the mac.
125+
126+
qvh gstreamer --pipeline "mp4mux name=mux ! filesink location=video.mp4 \
127+
queue name=audio_target ! wavparse ignore-length=true ! audioconvert ! faac ! aacparse ! mux. \
128+
queue name=video_target ! h264parse ! vtdec ! videoconvert ! x264enc tune=zerolatency ! mux."
129+
130+
Write MP4 file Linux:
131+
note that I am using software en and decoding, if you have intel VAAPI available, maybe use those.
132+
133+
gstreamer --pipeline "mp4mux name=mux ! filesink location=video.mp4 \
134+
queue name=audio_target ! wavparse ignore-length=true ! audioconvert ! avenc_aac ! aacparse ! mux. \
135+
queue name=video_target ! h264parse ! avdec_h264 ! videoconvert ! x264enc tune=zerolatency ! mux."
136+
`
137+
fmt.Print(examples)
138+
}
139+
140+
func startGStreamerWithCustomPipeline(udid string, pipelineString string) {
141+
log.Debug("Starting Gstreamer with custom pipeline")
142+
gStreamer, err := gstadapter.NewWithCustomPipeline(pipelineString)
143+
if err != nil {
144+
printErrJSON(err, "Failed creating custom pipeline")
145+
return
146+
}
147+
startWithConsumer(gStreamer, udid)
148+
}
149+
101150
func startGStreamer(udid string) {
102151
log.Debug("Starting Gstreamer")
103152
gStreamer := gstadapter.New()
@@ -194,9 +243,11 @@ func startWithConsumer(consumer screencapture.CmSampleBufConsumer, udid string)
194243
adapter := screencapture.UsbAdapter{}
195244
stopSignal := make(chan interface{})
196245
waitForSigInt(stopSignal)
246+
197247
mp := screencapture.NewMessageProcessor(&adapter, stopSignal, consumer)
198248

199249
err = adapter.StartReading(device, &mp, stopSignal)
250+
consumer.Stop()
200251
if err != nil {
201252
printErrJSON(err, "failed connecting to usb")
202253
}

screencapture/coremedia/avfilewriter.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ func (avfw AVFileWriter) Consume(buf CMSampleBuffer) error {
3030
return avfw.consumeVideo(buf)
3131
}
3232

33+
//Nothing currently
34+
func (avfw AVFileWriter) Stop() {}
35+
3336
func (avfw AVFileWriter) consumeVideo(buf CMSampleBuffer) error {
3437
if buf.HasFormatDescription {
3538
err := avfw.writeNalu(buf.FormatDescription.PPS)

screencapture/gstadapter/gst_adapter_linux.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ import (
1818
type GstAdapter struct {
1919
videoAppSrc *gst.AppSrc
2020
audioAppSrc *gst.AppSrc
21+
pipeline *gst.Pipeline
2122
firstAudioSample bool
2223
}
2324

25+
const audioAppSrcTargetElementName = "audio_target"
26+
const videoAppSrcTargetElementName = "video_target"
27+
2428
//New creates a new MAC OSX compatible gstreamer pipeline that will play device video and audio
2529
//in a nice little window :-D
2630
func New() *GstAdapter {
@@ -39,6 +43,79 @@ func New() *GstAdapter {
3943
return &gsta
4044
}
4145

46+
//NewWithCustomPipeline will parse the given pipelineString, connect the videoAppSrc to whatever element has the name "video_target" and the audioAppSrc to "audio_target"
47+
//see also: https://gstreamer.freedesktop.org/documentation/application-development/appendix/programs.html?gi-language=c
48+
func NewWithCustomPipeline(pipelineString string) (*GstAdapter, error) {
49+
log.Info("Starting Gstreamer..")
50+
log.WithFields(log.Fields{"custom_pipeline": pipelineString}).Debug("Starting Gstreamer with custom pipeline")
51+
pipeline, err := gst.ParseLaunch(pipelineString)
52+
if err != nil {
53+
return nil, fmt.Errorf("Invalid Pipeline, checkout --examples for help. Gstreamer parsing error was: %s", err)
54+
}
55+
56+
audioAppSrcTargetElement := pipeline.AsBin().GetByName(audioAppSrcTargetElementName)
57+
if audioAppSrcTargetElement == nil {
58+
return nil, fmt.Errorf("The pipeline needs an element with a property 'name=%s' so I can link the audio source to it. run with --examples for details.", audioAppSrcTargetElementName)
59+
}
60+
61+
videoAppSrcTargetElement := pipeline.AsBin().GetByName(videoAppSrcTargetElementName)
62+
if videoAppSrcTargetElement == nil {
63+
return nil, fmt.Errorf("The pipeline needs an element with a property 'name=%s' so I can link the video source to it. run with --examples for details.", videoAppSrcTargetElementName)
64+
}
65+
66+
videoAppSrc := gst.NewAppSrc("my-video-src")
67+
videoAppSrc.SetProperty("is-live", true)
68+
69+
audioAppSrc := gst.NewAppSrc("my-audio-src")
70+
audioAppSrc.SetProperty("is-live", true)
71+
72+
pipeline.Add(videoAppSrc.AsElement())
73+
pipeline.Add(audioAppSrc.AsElement())
74+
75+
audioAppSrc.Link(audioAppSrcTargetElement)
76+
videoAppSrc.Link(videoAppSrcTargetElement)
77+
78+
pipeline.SetState(gst.STATE_PLAYING)
79+
//runGlibMainLoop()
80+
81+
log.Info("Gstreamer is running!")
82+
gsta := GstAdapter{videoAppSrc: videoAppSrc, audioAppSrc: audioAppSrc, firstAudioSample: true, pipeline: pipeline}
83+
84+
return &gsta, nil
85+
}
86+
87+
//Stop sends an EOS (end of stream) event downstream the gstreamer pipeline.
88+
//Some Elements need this to correctly finish. F.ex. writing mp4 video without
89+
//sending EOS will result in a broken mp4 file
90+
func (gsta GstAdapter) Stop() {
91+
log.Info("Stopping Gstreamer..")
92+
success := gsta.audioAppSrc.SendEvent(gst.Eos())
93+
if !success {
94+
log.Warn("Failed sending EOS signal for audio app source")
95+
}
96+
success = gsta.videoAppSrc.SendEvent(gst.Eos())
97+
if !success {
98+
log.Warn("Failed sending EOS signal for video app source")
99+
}
100+
101+
bus := gsta.pipeline.GetBus()
102+
103+
//I hope those are 60 seconds
104+
msg := bus.TimedPopFiltered(1000000000*1000*60, gst.MESSAGE_EOS|gst.MESSAGE_ERROR)
105+
106+
if msg == nil {
107+
log.Warn("No EOS received, video files might be broken")
108+
return
109+
}
110+
if msg.GetType() == gst.MESSAGE_ERROR {
111+
log.Warn("Error received, video files might be broken")
112+
return
113+
}
114+
log.Info("EOS received")
115+
gsta.pipeline.SetState(gst.STATE_NULL)
116+
log.Info("Gstreamer finished")
117+
}
118+
42119
//runGlibMainLoop starts the glib Mainloop necessary for the video player to work on MAC OS X.
43120
func runGlibMainLoop() {
44121
go func() {

screencapture/gstadapter/gst_adapter_macos.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ import (
1818
type GstAdapter struct {
1919
videoAppSrc *gst.AppSrc
2020
audioAppSrc *gst.AppSrc
21+
pipeline *gst.Pipeline
2122
firstAudioSample bool
2223
}
2324

25+
const audioAppSrcTargetElementName = "audio_target"
26+
const videoAppSrcTargetElementName = "video_target"
27+
2428
//New creates a new MAC OSX compatible gstreamer pipeline that will play device video and audio
2529
//in a nice little window :-D
2630
func New() *GstAdapter {
@@ -39,6 +43,78 @@ func New() *GstAdapter {
3943
return &gsta
4044
}
4145

46+
//NewWithCustomPipeline will parse the given pipelineString, connect the videoAppSrc to whatever element has the name "video_target" and the audioAppSrc to "audio_target"
47+
//see also: https://gstreamer.freedesktop.org/documentation/application-development/appendix/programs.html?gi-language=c
48+
func NewWithCustomPipeline(pipelineString string) (*GstAdapter, error) {
49+
log.Info("Starting Gstreamer..")
50+
log.WithFields(log.Fields{"custom_pipeline": pipelineString}).Debug("Starting Gstreamer with custom pipeline")
51+
pipeline, err := gst.ParseLaunch(pipelineString)
52+
if err != nil {
53+
return nil, fmt.Errorf("Invalid Pipeline, checkout --examples for help. Gstreamer parsing error was: %s", err)
54+
}
55+
56+
audioAppSrcTargetElement := pipeline.AsBin().GetByName(audioAppSrcTargetElementName)
57+
if audioAppSrcTargetElement == nil {
58+
return nil, fmt.Errorf("The pipeline needs an element with a property 'name=%s' so I can link the audio source to it. run with --examples for details.", audioAppSrcTargetElementName)
59+
}
60+
61+
videoAppSrcTargetElement := pipeline.AsBin().GetByName(videoAppSrcTargetElementName)
62+
if videoAppSrcTargetElement == nil {
63+
return nil, fmt.Errorf("The pipeline needs an element with a property 'name=%s' so I can link the video source to it. run with --examples for details.", videoAppSrcTargetElementName)
64+
}
65+
66+
videoAppSrc := gst.NewAppSrc("my-video-src")
67+
videoAppSrc.SetProperty("is-live", true)
68+
69+
audioAppSrc := gst.NewAppSrc("my-audio-src")
70+
audioAppSrc.SetProperty("is-live", true)
71+
72+
pipeline.Add(videoAppSrc.AsElement())
73+
pipeline.Add(audioAppSrc.AsElement())
74+
75+
audioAppSrc.Link(audioAppSrcTargetElement)
76+
videoAppSrc.Link(videoAppSrcTargetElement)
77+
78+
pipeline.SetState(gst.STATE_PLAYING)
79+
//runGlibMainLoop()
80+
81+
log.Info("Gstreamer is running!")
82+
gsta := GstAdapter{videoAppSrc: videoAppSrc, audioAppSrc: audioAppSrc, firstAudioSample: true, pipeline: pipeline}
83+
84+
return &gsta, nil
85+
}
86+
87+
//Stop sends an EOS (end of stream) event downstream the gstreamer pipeline.
88+
//Some Elements need this to correctly finish. F.ex. writing mp4 video without
89+
//sending EOS will result in a broken mp4 file
90+
func (gsta GstAdapter) Stop() {
91+
log.Info("Stopping Gstreamer..")
92+
success := gsta.audioAppSrc.SendEvent(gst.Eos())
93+
if !success {
94+
log.Warn("Failed sending EOS signal for audio app source")
95+
}
96+
success = gsta.videoAppSrc.SendEvent(gst.Eos())
97+
if !success {
98+
log.Warn("Failed sending EOS signal for video app source")
99+
}
100+
101+
bus := gsta.pipeline.GetBus()
102+
103+
//I hope those are 60 seconds
104+
msg := bus.TimedPopFiltered(1000000000*1000*60, gst.MESSAGE_EOS|gst.MESSAGE_ERROR)
105+
if msg == nil {
106+
log.Warn("No EOS received, video files might be broken")
107+
return
108+
}
109+
if msg.GetType() == gst.MESSAGE_ERROR {
110+
log.Warn("Error received, video files might be broken")
111+
return
112+
}
113+
log.Info("EOS received")
114+
gsta.pipeline.SetState(gst.STATE_NULL)
115+
log.Info("Gstreamer finished")
116+
}
117+
42118
//runGlibMainLoop starts the glib Mainloop necessary for the video player to work on MAC OS X.
43119
func runGlibMainLoop() {
44120
go func() {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package gstadapter_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/danielpaulus/quicktime_video_hack/screencapture/gstadapter"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestCustomPipelineParsing(t *testing.T) {
11+
12+
_, err := gstadapter.NewWithCustomPipeline("daniel")
13+
assert.Error(t, err)
14+
15+
_, err = gstadapter.NewWithCustomPipeline("queue name=my_filesrc ! fakesink")
16+
assert.Error(t, err)
17+
18+
_, err = gstadapter.NewWithCustomPipeline("queue name=audio_target ! fakesink")
19+
assert.Error(t, err)
20+
21+
gsta, err := gstadapter.NewWithCustomPipeline("rtpmux name=mux ! fakesink \n queue name=audio_target ! mux.sink_0 \n queue name=video_target ! mux.sink_1")
22+
assert.NoError(t, err)
23+
assert.NotNil(t, gsta)
24+
}

screencapture/interfaces.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import "github.com/danielpaulus/quicktime_video_hack/screencapture/coremedia"
55
//CmSampleBufConsumer is a simple interface with one function that consumes CMSampleBuffers
66
type CmSampleBufConsumer interface {
77
Consume(buf coremedia.CMSampleBuffer) error
8+
Stop()
89
}
910

1011
//UsbDataReceiver should receive USB SYNC, ASYN and PING packets with the correct length and with the 4 bytes length removed.

0 commit comments

Comments
 (0)