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