diff --git a/.gitignore b/.gitignore index 39a4f9c..d4ab968 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ compiled/ # Dependency tracking files *.dep +/doc diff --git a/dlna-player.rkt b/dlna-player.rkt index dabd7b1..8493fda 100644 --- a/dlna-player.rkt +++ b/dlna-player.rkt @@ -15,7 +15,9 @@ (provide make-dlna-player dlna-player? dlna-player-play! + dlna-player-play-uri! dlna-player-set-next-file! + dlna-player-set-next-uri! dlna-player-pause! dlna-player-resume! dlna-player-stop! @@ -39,7 +41,7 @@ #:transparent) (struct publication - (track uri metadata) + (track uri metadata owned? protocol-info mime-type) #:transparent) (struct dlna-player @@ -214,10 +216,14 @@ #:genre (dlna-track-info-genre track) #:year (dlna-track-info-year track) #:track (dlna-track-info-track track) - #:duration (dlna-track-info-duration track)))))) + #:duration (dlna-track-info-duration track)) + #t + #f + #f)))) (define (safe-unpublish! player item) - (when item + (when (and item + (publication-owned? item)) (with-handlers ([exn:fail? (lambda (exception) (warn-dlna-player "Could not unpublish ~a: ~a" (publication-uri item) (exn-message exception)) (void))]) @@ -271,7 +277,9 @@ ([exn:fail? (lambda (exception) (err-dlna-player "Renderer operation failed: ~a" (exn-message exception)) - (set-reachable! player #f) + (set-reachable! + player + (exn:fail:upnp? exception)) (raise exception))]) (let ([result (call-with-semaphore @@ -339,7 +347,11 @@ ([exn:fail? (lambda (exception) (warn-dlna-player "Could not refresh renderer state: ~a" (exn-message exception)) - (set-reachable! player #f) + ;; A SOAP/UPnP error is still a valid response from the renderer. + ;; Only transport failures mean that it is unreachable. + (set-reachable! + player + (exn:fail:upnp? exception)) #f)]) (call-with-semaphore (dlna-player-transport-lock player) @@ -479,11 +491,8 @@ (thread (lambda () (poll-player player)))) player)) -(define (dlna-player-play! player file) - (info-dlna-player "Play requested file=~a" file) - (check-player 'dlna-player-play! player) - (let* ([track (player-track-info 'dlna-player-play! player file)] - [item (publish-track! player track)]) +(define (play-publication! player item) + (let ((track (publication-track item))) (with-handlers ([exn:fail? (lambda (exception) @@ -492,10 +501,33 @@ (call-renderer player (lambda () - (media-renderer-play-uri! - (dlna-player-renderer player) - (publication-uri item) - #:metadata (publication-metadata item)))) + (if (or (publication-protocol-info item) + (publication-mime-type item)) + (media-renderer-play-resource! + (dlna-player-renderer player) + (publication-uri item) + #:protocol-info (publication-protocol-info item) + #:mime-type (publication-mime-type item) + #:title + (if track + (dlna-track-info-title track) + "Media") + #:artist + (and track (dlna-track-info-artist track)) + #:album + (and track (dlna-track-info-album track)) + #:genre + (and track (dlna-track-info-genre track)) + #:year + (and track (dlna-track-info-year track)) + #:track + (and track (dlna-track-info-track track)) + #:duration + (and track (dlna-track-info-duration track))) + (media-renderer-play-uri! + (dlna-player-renderer player) + (publication-uri item) + #:metadata (publication-metadata item))))) (let ([old-current #f] [old-next #f]) (call-with-semaphore @@ -514,7 +546,8 @@ #f #f 0 - (dlna-track-info-duration track) + (and track + (dlna-track-info-duration track)) (dlna-info-volume (dlna-player-cached-info player)) (dlna-info-muted? (dlna-player-cached-info player)) #t)) @@ -523,15 +556,44 @@ (safe-unpublish! player old-next)) (void)))) -(define (dlna-player-set-next-file! player file) - (info-dlna-player "Set next track requested file=~a" file) - (check-player 'dlna-player-set-next-file! player) - (let* ([track - (player-track-info - 'dlna-player-set-next-file! - player - file)] +(define (dlna-player-play! player file) + (info-dlna-player "Play requested file=~a" file) + (check-player 'dlna-player-play! player) + (let* ([track (player-track-info 'dlna-player-play! player file)] [item (publish-track! player track)]) + (play-publication! player item))) + +(define (dlna-player-play-uri! player uri + #:metadata [metadata ""] + #:protocol-info [protocol-info #f] + #:mime-type [mime-type #f] + #:track [track #f]) + (info-dlna-player "Play requested URI=~a" uri) + (check-player 'dlna-player-play-uri! player) + (unless (and (string? uri) + (not (string=? (string-trim uri) ""))) + (raise-argument-error + 'dlna-player-play-uri! + "non-empty-string?" + uri)) + (unless (or (not track) + (dlna-track-info? track)) + (raise-argument-error + 'dlna-player-play-uri! + "(or/c #f dlna-track-info?)" + track)) + (for ([value (in-list (list protocol-info mime-type))]) + (unless (or (not value) (string? value)) + (raise-argument-error + 'dlna-player-play-uri! + "(or/c #f string?)" + value))) + (play-publication! + player + (publication track uri metadata #f protocol-info mime-type))) + +(define (set-next-publication! player item) + (let ((track (publication-track item))) (with-handlers ([exn:fail? (lambda (exception) @@ -540,10 +602,33 @@ (call-renderer player (lambda () - (media-renderer-set-next-uri! - (dlna-player-renderer player) - (publication-uri item) - #:metadata (publication-metadata item)))) + (if (or (publication-protocol-info item) + (publication-mime-type item)) + (media-renderer-set-next-resource! + (dlna-player-renderer player) + (publication-uri item) + #:protocol-info (publication-protocol-info item) + #:mime-type (publication-mime-type item) + #:title + (if track + (dlna-track-info-title track) + "Media") + #:artist + (and track (dlna-track-info-artist track)) + #:album + (and track (dlna-track-info-album track)) + #:genre + (and track (dlna-track-info-genre track)) + #:year + (and track (dlna-track-info-year track)) + #:track + (and track (dlna-track-info-track track)) + #:duration + (and track (dlna-track-info-duration track))) + (media-renderer-set-next-uri! + (dlna-player-renderer player) + (publication-uri item) + #:metadata (publication-metadata item))))) (let ([old-next #f]) (call-with-semaphore (dlna-player-cache-lock player) @@ -560,6 +645,46 @@ (safe-unpublish! player old-next)) (void)))) +(define (dlna-player-set-next-file! player file) + (info-dlna-player "Set next track requested file=~a" file) + (check-player 'dlna-player-set-next-file! player) + (let* ([track + (player-track-info + 'dlna-player-set-next-file! + player + file)] + [item (publish-track! player track)]) + (set-next-publication! player item))) + +(define (dlna-player-set-next-uri! player uri + #:metadata [metadata ""] + #:protocol-info [protocol-info #f] + #:mime-type [mime-type #f] + #:track [track #f]) + (info-dlna-player "Set next track requested URI=~a" uri) + (check-player 'dlna-player-set-next-uri! player) + (unless (and (string? uri) + (not (string=? (string-trim uri) ""))) + (raise-argument-error + 'dlna-player-set-next-uri! + "non-empty-string?" + uri)) + (unless (or (not track) + (dlna-track-info? track)) + (raise-argument-error + 'dlna-player-set-next-uri! + "(or/c #f dlna-track-info?)" + track)) + (for ([value (in-list (list protocol-info mime-type))]) + (unless (or (not value) (string? value)) + (raise-argument-error + 'dlna-player-set-next-uri! + "(or/c #f string?)" + value))) + (set-next-publication! + player + (publication track uri metadata #f protocol-info mime-type))) + (define (dlna-player-pause! player) (info-dlna-player "Pause requested") (check-player 'dlna-player-pause! player) @@ -595,10 +720,21 @@ (define (dlna-player-stop! player) (info-dlna-player "Stop requested") (check-player 'dlna-player-stop! player) - (call-renderer - player - (lambda () - (media-renderer-stop! (dlna-player-renderer player)))) + (with-handlers + ([exn:fail:upnp? + (lambda (exception) + (unless + (equal? + (exn:fail:upnp-code exception) + "701") + (raise exception)) + (dbg-dlna-player + "Renderer was already in a state where Stop is unavailable"))]) + (call-renderer + player + (lambda () + (media-renderer-stop! + (dlna-player-renderer player))))) (update-cache! player (lambda (info) diff --git a/scribblings/racket-audio-dlna.scrbl b/scribblings/racket-audio-dlna.scrbl index 0ecc979..58a6b24 100644 --- a/scribblings/racket-audio-dlna.scrbl +++ b/scribblings/racket-audio-dlna.scrbl @@ -60,6 +60,37 @@ Publishes and preloads the file with @tt{SetNextAVTransportURI}. Replacing the next file removes the previous next-file publication. } +@defproc[(dlna-player-play-uri! + [player dlna-player?] + [uri string?] + [#:metadata metadata string? ""] + [#:protocol-info protocol-info (or/c #f string?) #f] + [#:mime-type mime-type (or/c #f string?) #f] + [#:track track (or/c #f dlna-track-info?) #f]) + void?]{ + +Installs an existing HTTP media URI directly on the renderer and starts +playback. The URI is not published or owned by this player. Optional +DIDL-Lite metadata and track information are retained in the player state. +When @racket[protocol-info] or @racket[mime-type] is supplied, the resource +capabilities are resolved by @racketmodname[racket-upnp] immediately before +playback and matching DIDL-Lite is generated automatically. In that case +@racket[metadata] is ignored. +} + +@defproc[(dlna-player-set-next-uri! + [player dlna-player?] + [uri string?] + [#:metadata metadata string? ""] + [#:protocol-info protocol-info (or/c #f string?) #f] + [#:mime-type mime-type (or/c #f string?) #f] + [#:track track (or/c #f dlna-track-info?) #f]) + void?]{ + +Preloads an existing HTTP media URI with @tt{SetNextAVTransportURI}. Replacing +it never attempts to remove the external resource. +} + @defproc[(dlna-player-pause! [player dlna-player?]) void?] @defproc[(dlna-player-resume! [player dlna-player?]) void?] @defproc[(dlna-player-stop! [player dlna-player?]) void?] diff --git a/tests/interface-test.rkt b/tests/interface-test.rkt index 44a2a7e..38fbb5a 100644 --- a/tests/interface-test.rkt +++ b/tests/interface-test.rkt @@ -38,6 +38,8 @@ (check-equal? (dlna-info-position info) 12) (check-equal? (dlna-info-volume info) 35) (check-true (dlna-info-reachable? info)) +(check-true (procedure? dlna-player-play-uri!)) +(check-true (procedure? dlna-player-set-next-uri!)) (check-exn exn:fail:contract? (lambda () (make-dlna-player #f)))