245 lines
7.4 KiB
Racket
245 lines
7.4 KiB
Racket
#lang racket
|
|
|
|
(require racket/class
|
|
racket/path
|
|
racket/udp
|
|
racket-upnp
|
|
"utils.rkt")
|
|
|
|
(provide dlna-player%)
|
|
|
|
(define dlna-player%
|
|
(class object%
|
|
(init-field renderer
|
|
[port 8080]
|
|
[time-updater (lambda (time-s length-s) #t)]
|
|
[track-nr-updater (lambda (nr) #t)]
|
|
[state-updater (lambda (state) #t)]
|
|
[track-ended (lambda () #t)]
|
|
[track-changed (lambda (nr) #t)])
|
|
|
|
(define lock (make-semaphore 1))
|
|
(define server #f)
|
|
(define poll-thread #f)
|
|
(define stopped #f)
|
|
(define requested-stop #t)
|
|
(define current-state 'stopped)
|
|
(define current-track-nr #f)
|
|
(define current-duration 0)
|
|
(define uri->track-nr (make-hash))
|
|
|
|
(define (with-renderer f)
|
|
(call-with-semaphore lock f))
|
|
|
|
(define (local-address)
|
|
(let ((socket (udp-open-socket)))
|
|
(dynamic-wind
|
|
void
|
|
(lambda ()
|
|
(udp-connect! socket (media-renderer-address renderer) 1900)
|
|
(let-values (((address local-port remote-address remote-port)
|
|
(udp-addresses socket #t)))
|
|
address))
|
|
(lambda ()
|
|
(udp-close socket)))))
|
|
|
|
(define (start-server)
|
|
(let ((address (local-address)))
|
|
(info-rktplayer
|
|
"Starting DLNA media server on ~a:~a for ~a"
|
|
address
|
|
port
|
|
(media-renderer-name renderer))
|
|
(start-media-file-server
|
|
(format "http://~a:~a/media/" address port)
|
|
#:listen-ip address)))
|
|
|
|
(define (file-url file track-nr)
|
|
(let* ((extension (path-get-extension file))
|
|
(name (if extension
|
|
(format "track-~a~a" track-nr extension)
|
|
(format "track-~a" track-nr)))
|
|
(uri (media-file-server-publish! server file name)))
|
|
(hash-set! uri->track-nr uri track-nr)
|
|
uri))
|
|
|
|
(define (renderer-call what f)
|
|
(with-handlers
|
|
((exn:fail?
|
|
(lambda (exception)
|
|
(warn-rktplayer
|
|
"Could not ~a on DLNA renderer ~a: ~a"
|
|
what
|
|
(media-renderer-name renderer)
|
|
(exn-message exception))
|
|
#f)))
|
|
(with-renderer f)))
|
|
|
|
(define (update-track uri)
|
|
(let ((nr (and uri (hash-ref uri->track-nr uri #f))))
|
|
(when (and nr
|
|
(or (not current-track-nr)
|
|
(not (= nr current-track-nr))))
|
|
(set! current-track-nr nr)
|
|
(track-nr-updater nr)
|
|
(track-changed nr))))
|
|
|
|
(define (poll)
|
|
(let ((reported-state
|
|
(renderer-call
|
|
"read playback state"
|
|
(lambda ()
|
|
(media-renderer-status renderer)))))
|
|
(when reported-state
|
|
(let ((state (if (eq? reported-state 'no-media)
|
|
'stopped
|
|
reported-state)))
|
|
(unless (eq? state current-state)
|
|
(set! current-state state)
|
|
(state-updater state))
|
|
(when (or (eq? state 'playing)
|
|
(eq? state 'paused)
|
|
(eq? state 'transitioning))
|
|
(let ((position
|
|
(renderer-call
|
|
"read playback position"
|
|
(lambda ()
|
|
(media-renderer-position renderer)))))
|
|
(when position
|
|
(let ((seconds (transport-position-seconds position))
|
|
(duration (transport-position-duration position)))
|
|
(when (and seconds duration)
|
|
(set! current-duration duration)
|
|
(time-updater seconds duration))
|
|
(update-track (transport-position-uri position))))))
|
|
(when (and (eq? state 'stopped)
|
|
(not requested-stop))
|
|
(set! requested-stop #t)
|
|
(track-ended))))))
|
|
|
|
(define (poll-loop)
|
|
(let loop ()
|
|
(unless stopped
|
|
(with-handlers
|
|
((exn:fail?
|
|
(lambda (exception)
|
|
(warn-rktplayer
|
|
"DLNA polling failed for ~a: ~a"
|
|
(media-renderer-name renderer)
|
|
(exn-message exception)))))
|
|
(poll))
|
|
(sleep 0.5)
|
|
(loop))))
|
|
|
|
(define/public (name)
|
|
(media-renderer-name renderer))
|
|
|
|
(define/public (next-uri-supported?)
|
|
(and (renderer-call
|
|
"inspect supported actions"
|
|
(lambda ()
|
|
(media-renderer-next-uri-supported? renderer)))
|
|
#t))
|
|
|
|
(define/public (play-file! file track-nr
|
|
#:next-file (next-file #f)
|
|
#:next-track-nr (next-track-nr #f))
|
|
(let ((uri (file-url file track-nr))
|
|
(next-uri (and next-file
|
|
next-track-nr
|
|
(file-url next-file next-track-nr))))
|
|
(set! requested-stop #f)
|
|
(set! current-track-nr track-nr)
|
|
(track-nr-updater track-nr)
|
|
(state-updater 'transitioning)
|
|
(if next-uri
|
|
(with-renderer
|
|
(lambda ()
|
|
(media-renderer-play-uri!
|
|
renderer
|
|
uri
|
|
#:next-uri next-uri)))
|
|
(with-renderer
|
|
(lambda ()
|
|
(media-renderer-play-uri! renderer uri))))))
|
|
|
|
(define/public (set-next-file! file track-nr)
|
|
(let ((uri (file-url file track-nr)))
|
|
(with-renderer
|
|
(lambda ()
|
|
(media-renderer-set-next-uri! renderer uri)))))
|
|
|
|
(define/public (clear-next!)
|
|
(with-renderer
|
|
(lambda ()
|
|
(media-renderer-set-next-uri! renderer ""))))
|
|
|
|
(define/public (pause!)
|
|
(with-renderer
|
|
(lambda ()
|
|
(media-renderer-pause! renderer))))
|
|
|
|
(define/public (play!)
|
|
(with-renderer
|
|
(lambda ()
|
|
(media-renderer-play! renderer))))
|
|
|
|
(define/public (stop!)
|
|
(set! requested-stop #t)
|
|
(with-renderer
|
|
(lambda ()
|
|
(media-renderer-stop! renderer))))
|
|
|
|
(define/public (seek! percentage)
|
|
(when (> current-duration 0)
|
|
(with-renderer
|
|
(lambda ()
|
|
(media-renderer-seek!
|
|
renderer
|
|
(* current-duration (/ percentage 100.0)))))))
|
|
|
|
(define/public (volume)
|
|
(or (renderer-call
|
|
"read volume"
|
|
(lambda ()
|
|
(media-renderer-volume renderer)))
|
|
0))
|
|
|
|
(define/public (set-volume! percentage)
|
|
(renderer-call
|
|
"set volume"
|
|
(lambda ()
|
|
(media-renderer-set-volume!
|
|
renderer
|
|
(max 0
|
|
(min 100
|
|
(inexact->exact
|
|
(round percentage))))))))
|
|
|
|
(define/public (quit)
|
|
(set! requested-stop #t)
|
|
(set! stopped #t)
|
|
(when poll-thread
|
|
(kill-thread poll-thread))
|
|
(with-handlers
|
|
((exn:fail?
|
|
(lambda (exception)
|
|
(dbg-rktplayer
|
|
"Could not stop DLNA renderer while quitting: ~a"
|
|
(exn-message exception)))))
|
|
(with-renderer
|
|
(lambda ()
|
|
(media-renderer-stop! renderer))))
|
|
(when server
|
|
(media-file-server-stop! server)
|
|
(set! server #f)))
|
|
|
|
(super-new)
|
|
|
|
(begin
|
|
(set! server (start-server))
|
|
(set! poll-thread (thread poll-loop))
|
|
(info-rktplayer
|
|
"DLNA player initialized for ~a"
|
|
(media-renderer-name renderer)))))
|