golang: add plaincast with dreambox-dbus (requires latest enigma2) patch and its...
[opendreambox.git] / meta-opendreambox / recipes-devtools / golang / golang-plaincast / 0001-implement-dbus-based-dreambox-player.patch
1 From 5ea1a278438d85ba9b39e6a5c01a1855c7e1d422 Mon Sep 17 00:00:00 2001
2 From: Stephan Reichholf <reichi@opendreambox.org>
3 Date: Wed, 5 Jul 2017 12:19:28 +0200
4 Subject: [PATCH] implement dbus based dreambox player
5
6 ---
7  github.com/aykevl/plaincast/apps/youtube/mp/mpdbus.go  | 168 +++++++++++++++++++++++++++++++++++++++++++++
8  github.com/aykevl/plaincast/apps/youtube/mp/player.go  |   8 ++-
9  github.com/aykevl/plaincast/apps/youtube/mp/youtube.go | 127 +---------------------------------
10  github.com/aykevl/plaincast/plaincast.service          |   7 +-
11  4 files changed, 177 insertions(+), 133 deletions(-)
12  create mode 100644 github.com/aykevl/plaincast/apps/youtube/mp/mpdbus.go
13
14 diff --git a/github.com/aykevl/plaincast/apps/youtube/mp/mpdbus.go b/github.com/aykevl/plaincast/apps/youtube/mp/mpdbus.go
15 new file mode 100644
16 index 0000000..ed1c4b7
17 --- /dev/null
18 +++ b/github.com/aykevl/plaincast/apps/youtube/mp/mpdbus.go
19 @@ -0,0 +1,168 @@
20 +package mp
21 +
22 +import (
23 +       "flag"
24 +       "fmt"
25 +       "sync"
26 +       "time"
27 +
28 +       "github.com/aykevl/plaincast/config"
29 +       "github.com/aykevl/plaincast/log"
30 +       "github.com/godbus/dbus"
31 +)
32 +
33 +type MPDBUS struct {
34 +       connection   *dbus.Conn
35 +       object       dbus.BusObject
36 +       running      bool
37 +       runningMutex sync.Mutex
38 +       eventChan    chan State
39 +       dbusChan     chan *dbus.Signal
40 +}
41 +
42 +var mpdbusLogger = log.New("dbus", "log DBus wrapper output")
43 +var logMPDBUS = flag.Bool("log-mpdbus", false, "log output of dbus mp")
44 +
45 +func (mpdbus *MPDBUS) initialize() (chan State, int) {
46 +       if mpdbus.connection != nil || mpdbus.running {
47 +               panic("already initialized")
48 +       }
49 +
50 +       conn, err := dbus.SystemBus()
51 +       if err != nil {
52 +               panic(err)
53 +       }
54 +       mpdbus.connection = conn
55 +       mpdbus.object = conn.Object("com.dreambox.ctl", "/com/dreambox/ctl")
56 +       mpdbus.running = true
57 +
58 +       conf := config.Get()
59 +       initialVolume, err := conf.GetInt("player.mpdbus.volume", func() (int, error) {
60 +               return INITIAL_VOLUME, nil
61 +       })
62 +       if err != nil {
63 +               // should not happen
64 +               panic(err)
65 +       }
66 +
67 +       mpdbus.connection.BusObject().Call("org.freedesktop.DBus.AddMatch", 0,
68 +               "type='signal', interface='com.dreambox.ctl', member='event'")
69 +       mpdbus.dbusChan = make(chan *dbus.Signal, 20)
70 +       mpdbus.connection.Signal(mpdbus.dbusChan)
71 +
72 +       go mpdbus.dbusEventHandler(mpdbus.dbusChan)
73 +
74 +       mpdbus.eventChan = make(chan State)
75 +       return mpdbus.eventChan, initialVolume
76 +}
77 +
78 +func (mpdbus *MPDBUS) quit() {
79 +       mpdbus.running = false
80 +       mpdbus.stop()
81 +}
82 +
83 +func (mpdbus *MPDBUS) play(videoId string, position time.Duration, volume int) {
84 +       pos := position.Seconds()
85 +       uri := fmt.Sprintf("yt://%s", videoId)
86 +       logger.Println("MPDBUS uri:", uri)
87 +       logger.Println("MPDBUS pos:", pos)
88 +       var res bool
89 +       err := mpdbus.object.Call("com.dreambox.ctl.play", 0, uri, true).Store(&res)
90 +       if err != nil {
91 +               panic(err)
92 +       }
93 +       logger.Println("play result is", res)
94 +
95 +       if position.Seconds() > 0 {
96 +               mpdbus.setPosition(position)
97 +       }
98 +       mpdbus.setVolume(volume)
99 +}
100 +
101 +func (mpdbus *MPDBUS) pause() {
102 +       var res bool
103 +       err := mpdbus.object.Call("com.dreambox.ctl.pause", 0).Store(&res)
104 +       if err != nil {
105 +               panic(err)
106 +       }
107 +}
108 +
109 +func (mpdbus *MPDBUS) resume() {
110 +       var res bool
111 +       err := mpdbus.object.Call("com.dreambox.ctl.resume", 0).Store(&res)
112 +       if err != nil {
113 +               panic(err)
114 +       }
115 +}
116 +
117 +func (mpdbus *MPDBUS) getPosition() (time.Duration, error) {
118 +       var pos int64
119 +       err := mpdbus.object.Call("com.dreambox.ctl.getPosition", 0).Store(&pos)
120 +       if err != nil {
121 +               panic(err)
122 +       }
123 +       logger.Println("position: ", pos)
124 +       if pos < 0 {
125 +               pos = 0
126 +       }
127 +       return time.Duration(pos * int64(time.Second)), nil
128 +}
129 +
130 +func (mpdbus *MPDBUS) setPosition(position time.Duration) {
131 +       var res bool
132 +       err := mpdbus.object.Call("com.dreambox.ctl.setPosition", 0, int64(position.Seconds())).Store(&res)
133 +       if err != nil {
134 +               panic(err)
135 +       }
136 +}
137 +
138 +func (mpdbus *MPDBUS) getVolume() int {
139 +       var vol int
140 +       err := mpdbus.object.Call("com.dreambox.ctl.getVolume", 0).Store(&vol)
141 +       if err != nil {
142 +               panic(err)
143 +       }
144 +       return vol
145 +}
146 +
147 +func (mpdbus *MPDBUS) setVolume(volume int) {
148 +       var res bool
149 +       err := mpdbus.object.Call("com.dreambox.ctl.setVolume", 0, volume).Store(&res)
150 +       if err != nil {
151 +               panic(err)
152 +       }
153 +}
154 +
155 +func (mpdbus *MPDBUS) stop() {
156 +       go func() { mpdbus.eventChan <- STATE_STOPPED }()
157 +       var res bool
158 +       err := mpdbus.object.Call("com.dreambox.ctl.stop", 0).Store(&res)
159 +       if err != nil {
160 +               panic(err)
161 +       }
162 +}
163 +
164 +func (mpdbus *MPDBUS) dbusEventHandler(dbusEventChan chan *dbus.Signal) {
165 +       for v := range dbusEventChan {
166 +               logger.Println(v.Body)
167 +               eventId := v.Body[0].(int32)
168 +               var state State
169 +               if eventId == 0 {
170 +                       state = STATE_STOPPED
171 +               }
172 +               if eventId == 1 {
173 +                       state = STATE_PLAYING
174 +               }
175 +               if eventId == 2 {
176 +                       state = STATE_PAUSED
177 +               }
178 +               if eventId == 3 {
179 +                       state = STATE_BUFFERING
180 +                       return //Ignore buffering
181 +               }
182 +               if eventId == 4 {
183 +                       state = STATE_SEEKING
184 +               }
185 +               mpdbus.eventChan <- state
186 +       }
187 +}
188 diff --git a/github.com/aykevl/plaincast/apps/youtube/mp/player.go b/github.com/aykevl/plaincast/apps/youtube/mp/player.go
189 index 2418ea0..e853f1c 100644
190 --- a/github.com/aykevl/plaincast/apps/youtube/mp/player.go
191 +++ b/github.com/aykevl/plaincast/apps/youtube/mp/player.go
192 @@ -22,7 +22,7 @@ func New(stateChange chan StateChange) *MediaPlayer {
193         p.playstateChan = make(chan PlayState)
194         p.vg = NewVideoGrabber()
195  
196 -       p.player = &MPV{}
197 +       p.player = &MPDBUS{}
198         playerEventChan, initialVolume := p.player.initialize()
199  
200         // Start the mainloop.
201 @@ -44,6 +44,7 @@ func (p *MediaPlayer) Quit() {
202  func (p *MediaPlayer) getPosition(ps *PlayState) time.Duration {
203         var position time.Duration
204  
205 +       logger.Println("State is ", ps.State)
206         switch ps.State {
207         case STATE_STOPPED:
208                 position = 0
209 @@ -117,7 +118,7 @@ func (p *MediaPlayer) startPlaying(ps *PlayState, position time.Duration) {
210                 //  *  On very slow systems, like the Raspberry Pi, downloading the
211                 //     stream URL for the next video doesn't interrupt the currently
212                 //     playing video.
213 -               p.player.stop()
214 +               // p.player.stop()
215         }
216         p.setPlayState(ps, STATE_BUFFERING, position)
217  
218 @@ -365,7 +366,6 @@ func (p *MediaPlayer) Seek(position time.Duration) {
219                 if ps.State == STATE_STOPPED {
220                         p.startPlaying(ps, position)
221                 } else if ps.State == STATE_PAUSED || ps.State == STATE_PLAYING {
222 -                       p.setPlayState(ps, STATE_SEEKING, position)
223                         p.player.setPosition(position)
224                 } else {
225                         logger.Warnf("state is not paused or playing while seeking (state: %d) - ignoring\n", ps.State)
226 @@ -450,7 +450,9 @@ func (p *MediaPlayer) run(playerEventChan chan State, initialVolume int) {
227                         ps = <-p.playstateChan
228  
229                 case event, ok := <-playerEventChan:
230 +                       logger.Println("event: ", event, "# ok: ", ok)
231                         if !ok {
232 +                               logger.Println("Player has quit! Closing channels")
233                                 // player has quit, and closed channel
234                                 close(p.stateChange)
235                                 close(p.playstateChan)
236 diff --git a/github.com/aykevl/plaincast/apps/youtube/mp/youtube.go b/github.com/aykevl/plaincast/apps/youtube/mp/youtube.go
237 index 44d0ce1..110f187 100644
238 --- a/github.com/aykevl/plaincast/apps/youtube/mp/youtube.go
239 +++ b/github.com/aykevl/plaincast/apps/youtube/mp/youtube.go
240 @@ -1,126 +1,25 @@
241  package mp
242  
243  import (
244 -       "bufio"
245 -       "io"
246         "net/url"
247 -       "os"
248 -       "os/exec"
249         "strconv"
250         "sync"
251         "time"
252  )
253  
254 -const pythonGrabber = `
255 -try:
256 -    import sys
257 -    from youtube_dl import YoutubeDL
258 -    from youtube_dl.utils import DownloadError
259 -
260 -    if len(sys.argv) != 3:
261 -        sys.stderr.write('arguments: <format string> <cache dir>')
262 -        os.exit(1)
263 -
264 -    yt = YoutubeDL({
265 -        'geturl': True,
266 -        'format': sys.argv[1],
267 -        'cachedir': sys.argv[2] or None,
268 -        'quiet': True,
269 -        'simulate': True})
270 -
271 -    while True:
272 -        stream = ''
273 -        try:
274 -            url = sys.stdin.readline().strip()
275 -            stream = yt.extract_info(url, ie_key='Youtube')['url']
276 -        except (KeyboardInterrupt, EOFError, IOError):
277 -            break
278 -        except DownloadError as why:
279 -            # error message has already been printed
280 -            sys.stderr.write('Could not extract video, try updating youtube-dl.\n')
281 -        finally:
282 -            try:
283 -                sys.stdout.write(stream + '\n')
284 -                sys.stdout.flush()
285 -            except:
286 -                pass
287 -
288 -except (KeyboardInterrupt, EOFError, IOError):
289 -    pass
290 -`
291 -
292 -// First (mkv-container) audio only with 100+kbps, then video with audio
293 -// bitrate 100+ (where video has the lowest possible quality), then
294 -// slightly lower quality audio.
295 -// We do this because for some reason DASH aac audio (in the MP4 container)
296 -// doesn't support seeking in any of the tested players (mpv using
297 -// libavformat, and vlc, gstreamer and mplayer2 using their own demuxers).
298 -// But the MKV container seems to have much better support.
299 -// See:
300 -//   https://github.com/mpv-player/mpv/issues/579
301 -//   https://trac.ffmpeg.org/ticket/3842
302 -const grabberFormats = "171/172/43/22/18"
303 -
304  type VideoGrabber struct {
305         streams      map[string]*VideoURL // map of video ID to stream gotten from youtube-dl
306         streamsMutex sync.Mutex
307 -       cmd          *exec.Cmd
308 -       cmdMutex     sync.Mutex
309 -       cmdStdin     io.Writer
310 -       cmdStdout    *bufio.Reader
311  }
312  
313  func NewVideoGrabber() *VideoGrabber {
314         vg := VideoGrabber{}
315         vg.streams = make(map[string]*VideoURL)
316  
317 -       cacheDir := *cacheDir
318 -       if cacheDir != "" {
319 -               cacheDir = cacheDir + "/" + "youtube-dl"
320 -       }
321 -
322 -       // Start the process in a separate goroutine.
323 -       vg.cmdMutex.Lock()
324 -       go func() {
325 -               defer vg.cmdMutex.Unlock()
326 -
327 -               vg.cmd = exec.Command("python", "-c", pythonGrabber, grabberFormats, cacheDir)
328 -               stdout, err := vg.cmd.StdoutPipe()
329 -               if err != nil {
330 -                       logger.Fatal(err)
331 -               }
332 -               vg.cmdStdout = bufio.NewReader(stdout)
333 -               vg.cmdStdin, err = vg.cmd.StdinPipe()
334 -               if err != nil {
335 -                       logger.Fatal(err)
336 -               }
337 -               vg.cmd.Stderr = os.Stderr
338 -               err = vg.cmd.Start()
339 -               if err != nil {
340 -                       logger.Fatal("Could not start video stream grabber:", err)
341 -               }
342 -
343 -       }()
344 -
345         return &vg
346  }
347  
348  func (vg *VideoGrabber) Quit() {
349 -       vg.cmdMutex.Lock()
350 -       defer vg.cmdMutex.Unlock()
351 -
352 -       err := vg.cmd.Process.Signal(os.Interrupt)
353 -       if err != nil {
354 -               logger.Fatal("could not send SIGINT:", err)
355 -       }
356 -
357 -       // Wait until exit, and free resources
358 -       err = vg.cmd.Wait()
359 -       if err != nil {
360 -               if _, ok := err.(*exec.ExitError); !ok {
361 -                       logger.Fatal("process could not be stopped:", err)
362 -               }
363 -       }
364  }
365  
366  // GetStream returns the stream for videoId, or an empty string if an error
367 @@ -151,32 +50,8 @@ func (vg *VideoGrabber) getStream(videoId string) *VideoURL {
368  
369         // Streams normally expire in 6 hour, give it a margin of one hour.
370         stream = &VideoURL{videoId: videoId, expires: time.Now().Add(5 * time.Hour)}
371 -       stream.fetchMutex.Lock()
372 -
373         vg.streams[videoId] = stream
374 -
375 -       go func() {
376 -               vg.cmdMutex.Lock()
377 -               defer vg.cmdMutex.Unlock()
378 -
379 -               io.WriteString(vg.cmdStdin, videoURL+"\n")
380 -               line, err := vg.cmdStdout.ReadString('\n')
381 -               if err != nil {
382 -                       logger.Fatal("could not grab video:", err)
383 -               }
384 -
385 -               stream.url = line[:len(line)-1]
386 -               stream.fetchMutex.Unlock()
387 -
388 -               logger.Println("Got stream for", videoURL)
389 -
390 -               expires, err := getExpiresFromURL(stream.url)
391 -               if err != nil {
392 -                       logger.Warnln("failed to extract expires from video URL:", err)
393 -               } else if expires.Before(stream.expires) {
394 -                       logger.Warnln("URL expires before the estimated expires!")
395 -               }
396 -       }()
397 +       stream.url = videoId
398  
399         return stream
400  }
401 diff --git a/github.com/aykevl/plaincast/plaincast.service b/github.com/aykevl/plaincast/plaincast.service
402 index 97739e0..aa38570 100644
403 --- a/github.com/aykevl/plaincast/plaincast.service
404 +++ b/github.com/aykevl/plaincast/plaincast.service
405 @@ -1,11 +1,10 @@
406  [Unit]
407  Description=Plaincast service
408 -After=network.target sound.target
409 +After=network.target enigma2.service
410  
411  [Service]
412 -ExecStart=/usr/local/bin/plaincast -log-mpv -log-youtube -config /var/local/plaincast/plaincast.conf -cachedir /var/local/plaincast/cache
413 -User=plaincast
414 -Group=audio
415 +ExecStart=/usr/bin/plaincast -loglevel=info -config /etc/plaincast.conf
416 +Restart=always
417  
418  [Install]
419  WantedBy=multi-user.target
420 -- 
421 2.7.4
422