diff --git a/examples/media-file-server-example.rkt b/examples/media-file-server-example.rkt new file mode 100644 index 0000000..8cfdccf --- /dev/null +++ b/examples/media-file-server-example.rkt @@ -0,0 +1,35 @@ +#lang racket/base + +(require racket-upnp) + +(define files + (start-media-file-server + "http://10.7.3.118:8080/media/" + #:listen-ip "10.7.3.118")) + +(define current-uri + (media-file-server-publish! + files + "C:/muziek/current.flac" + "current.flac")) + +(define next-uri + (media-file-server-publish! + files + "C:/muziek/next.flac" + "next.flac")) + +(define renderer + (get-media-renderer "Denon")) + +(when renderer + (media-renderer-play-uri! + renderer + current-uri + #:next-uri next-uri)) + +;; Call this after playback no longer needs the URLs: +;; +;; (media-file-server-unpublish! files current-uri) +;; (media-file-server-unpublish! files next-uri) +;; (media-file-server-stop! files) diff --git a/main.rkt b/main.rkt index ae72faa..72ff83b 100644 --- a/main.rkt +++ b/main.rkt @@ -1,23 +1,17 @@ #lang racket/base ;; Main public interface for racket-upnp. -;; -;; This module provides: -;; - generic UPnP device discovery and service inspection; -;; - the high-level media-renderer interface; -;; - the high-level media-server browser interface. -;; -;; Service-specific interfaces remain available through: -;; racket-upnp/services/ (require "query.rkt" "service.rkt" "media-renderer.rkt" - "media-server.rkt") + "media-server.rkt" + "media-file-server.rkt") (provide (all-from-out "query.rkt" "service.rkt" "media-renderer.rkt" - "media-server.rkt")) + "media-server.rkt" + "media-file-server.rkt")) diff --git a/scribblings/media-file-server.scrbl b/scribblings/media-file-server.scrbl new file mode 100644 index 0000000..b37b490 --- /dev/null +++ b/scribblings/media-file-server.scrbl @@ -0,0 +1,113 @@ +#lang scribble/manual + +@(require + (for-label + racket/base + (file "../media-file-server.rkt"))) + +@title{Publishing Media Files} + +@defmodule[(file "../media-file-server.rkt")] + +This module publishes local files through a small Racket web server. The +underlying Racket file dispatcher streams files and supports both @tt{HEAD} +requests and HTTP byte ranges. Byte ranges are important for media-renderer +probing and seeking. + +@defproc[(start-media-file-server + [url string?] + [#:listen-ip listen-ip (or/c #f string?) #f]) + media-file-server?]{ + +Starts a server for the HTTP origin and path prefix in @racket[url]. +The URL must be an absolute @tt{http://} URL. A trailing slash is added when +needed. + +When @racket[listen-ip] is @racket[#f], the server listens on all local +interfaces. Supply a local address, such as @tt{10.7.3.118}, to restrict the +listener to that interface. +} + +@defproc[(media-file-server-url + [server media-file-server?]) + string?]{ + +Returns the normalized base URL of @racket[server]. +} + +@defproc[(media-file-server-publish! + [server media-file-server?] + [file path-string?] + [url string?] + [#:mime-type mime-type (or/c #f bytes? string?) #f]) + string?]{ + +Publishes @racket[file] at @racket[url] and returns the absolute URL. + +The URL may be relative to the server URL: + +@racketblock[ +(define uri + (media-file-server-publish! + files + "C:/muziek/test.flac" + "test.flac")) +] + +It may also be the complete URL, provided that its origin and path prefix match +the server: + +@racketblock[ +(media-file-server-publish! + files + "C:/muziek/test.flac" + "http://10.7.3.118:8080/media/test.flac") +] + +The MIME type is inferred for common audio, video, and image extensions. +Use @racket[#:mime-type] to override it. +} + +@defproc[(media-file-server-unpublish! + [server media-file-server?] + [url string?]) + void?]{ + +Removes the URL mapping. Requests that are already in progress may still +finish. +} + +@defproc[(media-file-server-stop! + [server media-file-server?]) + void?]{ + +Stops the web server. Repeated calls have no effect. +} + +@section{Playing local files} + +@racketblock[ +(define files + (start-media-file-server + "http://10.7.3.118:8080/media/" + #:listen-ip "10.7.3.118")) + +(define first-uri + (media-file-server-publish! + files + "C:/muziek/first.flac" + "first.flac")) + +(define second-uri + (media-file-server-publish! + files + "C:/muziek/second.flac" + "second.flac")) + +(media-renderer-play-uri! + renderer + first-uri + #:next-uri second-uri) +] + +Keep the URLs published until the renderer has finished reading them. diff --git a/scribblings/media-renderer.scrbl b/scribblings/media-renderer.scrbl index c4d3b66..5204532 100644 --- a/scribblings/media-renderer.scrbl +++ b/scribblings/media-renderer.scrbl @@ -1,51 +1,114 @@ #lang scribble/manual -@(require (for-label racket/base - racket/contract - (file "../main.rkt") - (file "../media-renderer.rkt") - (file "../services/av-transport.rkt"))) +@(require + (for-label + racket/base + (file "../media-renderer.rkt"))) -@title{Media Renderer} +@title{Media Renderers} -@defmodule[@racketmodname[racket-upnp/media-renderer] #:module-paths ((file "../media-renderer.rkt"))] +@defmodule[(file "../media-renderer.rkt")] -This module hides AVTransport and RenderingControl for common playback code. -The returned renderer remains an @racket[upnp-device?], so generic service -inspection is still available. +This module provides a device-oriented interface for UPnP and DLNA media +renderers. It hides the AVTransport and RenderingControl services used for +common playback and volume operations. -@defproc[(query-media-renderers [#:interface interface (or/c #f string?) #f] - [#:dns? dns? boolean? #f] - [#:dlna-only? dlna-only? boolean? #f]) - (listof upnp-device?)]{ -Discovers UPnP MediaRenderers. When @racket[dlna-only?] is true, only devices -advertising an @tt{X_DLNADOC} value are returned. +@defproc[(query-media-renderers + [#:interface interface (or/c #f string?) #f] + [#:dns? dns? boolean? #f] + [#:dlna-only? dlna-only? boolean? #f]) + (listof media-renderer?)]{ + +Discovers media renderers. When @racket[dlna-only?] is true, only devices that +advertise a DLNA document identifier are returned. } -@defproc[(media-renderer? [value any/c]) boolean?]{Recognises a MediaRenderer device.} -@defproc[(media-renderer-dlna? [renderer media-renderer?]) boolean?]{Reports whether the device description advertises a DLNA document identifier.} -@defproc[(media-renderer-play-uri! [renderer media-renderer?] - [uri string?] - [#:metadata metadata string? ""]) void?]{Sets the URI and starts playback.} -@defproc[(media-renderer-pause! [renderer media-renderer?]) void?]{Pauses playback.} -@defproc[(media-renderer-stop! [renderer media-renderer?]) void?]{Stops playback.} -@defproc[(media-renderer-seek! [renderer media-renderer?] [seconds nonnegative-real?]) void?]{Seeks to a relative time.} -@defproc[(media-renderer-status [renderer media-renderer?]) symbol?]{Returns the abstracted transport state.} -@defproc[(media-renderer-position [renderer media-renderer?]) transport-position?]{Returns current position information.} -@defproc[(media-renderer-volume [renderer media-renderer?]) (or/c #f exact-integer?)]{Returns the device-specific volume value.} -@defproc[(media-renderer-set-volume! [renderer media-renderer?] [volume (integer-in 0 65535)]) void?]{Sets the device-specific volume value.} -@defproc[(media-renderer-muted? [renderer media-renderer?]) boolean?]{Returns the mute state.} -@defproc[(media-renderer-set-muted! [renderer media-renderer?] [muted? boolean?]) void?]{Sets the mute state.} +@defproc[(get-media-renderer [filter-name string?]) + (or/c #f media-renderer?)]{ -@section[#:tag "media-renderer-example"]{Example} +Returns the first discovered renderer whose name contains +@racket[filter-name], ignoring case, or @racket[#f] when no renderer matches. +} -@racketblock[ -(define renderer - (car (query-media-renderers #:interface "10.7.3.118"))) +@defproc[(media-renderer? [value any/c]) boolean?] +@defproc[(media-renderer-name [renderer media-renderer?]) string?] +@defproc[(media-renderer-address [renderer media-renderer?]) string?] +@defproc[(media-renderer-manufacturer [renderer media-renderer?]) + (or/c #f string?)] +@defproc[(media-renderer-model [renderer media-renderer?]) + (or/c #f string?)] +@defproc[(media-renderer-dlna? [renderer media-renderer?]) boolean?] -(media-renderer-play-uri! - renderer - "http://10.7.3.32:50002/music/track.flac") +@section{Playback} -(media-renderer-set-volume! renderer 30) -] +@defproc[(media-renderer-play-uri! + [renderer media-renderer?] + [uri string?] + [#:metadata metadata string? ""] + [#:next-uri next-uri (or/c #f string?) #f] + [#:next-metadata next-metadata string? ""]) + any]{ + +Sets @racket[uri] as the current resource and starts playback. + +When @racket[next-uri] is supplied, it is installed with +@tt{SetNextAVTransportURI} before playback starts. A supporting renderer can +open and buffer the next resource while the current resource is playing, +allowing a gapless or near-gapless transition. +} + +@defproc[(media-renderer-next-uri-supported? + [renderer media-renderer?]) + boolean?]{ + +Reports whether the renderer advertises the optional +@tt{SetNextAVTransportURI} action. +} + +@defproc[(media-renderer-set-next-uri! + [renderer media-renderer?] + [uri string?] + [#:metadata metadata string? ""]) + any]{ + +Sets the resource that follows the currently playing resource. The action +describes one next resource, not a complete queue. After playback advances to +that resource, call this function again to preload the following track. +} + +@defproc[(media-renderer-pause! [renderer media-renderer?]) any] +@defproc[(media-renderer-stop! [renderer media-renderer?]) any] +@defproc[(media-renderer-seek! + [renderer media-renderer?] + [seconds exact-nonnegative-integer?]) + any] +@defproc[(media-renderer-status [renderer media-renderer?]) symbol?] +@defproc[(media-renderer-position [renderer media-renderer?]) + transport-position?] + +@defproc[(transport-position-track + [position transport-position?]) + exact-nonnegative-integer?] +@defproc[(transport-position-seconds + [position transport-position?]) + (or/c #f number?)] +@defproc[(transport-position-duration + [position transport-position?]) + (or/c #f number?)] +@defproc[(transport-position-uri + [position transport-position?]) + (or/c #f string?)] + +@section{Volume and mute} + +@defproc[(media-renderer-volume [renderer media-renderer?]) + exact-nonnegative-integer?] +@defproc[(media-renderer-set-volume! + [renderer media-renderer?] + [volume exact-nonnegative-integer?]) + any] +@defproc[(media-renderer-muted? [renderer media-renderer?]) boolean?] +@defproc[(media-renderer-set-muted! + [renderer media-renderer?] + [muted? boolean?]) + any] diff --git a/scribblings/media-server.scrbl b/scribblings/media-server.scrbl index 73d9d2b..db5f2ed 100644 --- a/scribblings/media-server.scrbl +++ b/scribblings/media-server.scrbl @@ -1,114 +1,120 @@ #lang scribble/manual -@(require (for-label racket/base - racket/contract - racket/list - (file "../main.rkt") - (file "../media-server.rkt"))) +@(require + (for-label + racket/base + (file "../media-server.rkt"))) -@title{Media Server Browser} +@title{Media Servers} -@defmodule[@racketmodname[racket-upnp/media-server] #:module-paths ((file "../media-server.rkt"))] +@defmodule[(file "../media-server.rkt")] -This module turns the DIDL-Lite returned by -@racketmodname[racket-upnp/services/content-directory] into ordinary Racket -values. Containers are browsed one level at a time; the module does not -recursively load an entire media library. This is suitable for a browser that -requests children when the user expands a container. +This module provides a higher-level browser for UPnP and DLNA media servers. +It converts ContentDirectory DIDL-Lite XML into containers, items, and media +resources. -The hierarchy is the logical hierarchy published by the media server. A server -may offer views such as artist, album, genre, and physical folder, so a track -can occur in more than one container. +@defproc[(query-media-servers + [#:interface interface (or/c #f string?) #f] + [#:dns? dns? boolean? #f]) + (listof media-server?)]{ + +Discovers media servers. +} + +@defproc[(get-media-server [filter-name string?]) + (or/c #f media-server?)]{ + +Returns the first discovered media server whose name contains +@racket[filter-name], ignoring case, or @racket[#f]. +} + +@defproc[(media-server? [value any/c]) boolean?] +@defproc[(media-server-name [server media-server?]) string?] +@defproc[(media-server-address [server media-server?]) string?] +@defproc[(media-server-manufacturer [server media-server?]) + (or/c #f string?)] +@defproc[(media-server-model [server media-server?]) + (or/c #f string?)] @section{Browsing} -@defproc[(query-media-servers [#:interface interface (or/c #f string?) #f] - [#:dns? dns? boolean? #f]) - (listof media-server?)]{ -Discovers UPnP MediaServer devices that provide a ContentDirectory service. -} - -@defproc[(media-server? [value any/c]) boolean?]{ -Recognises a MediaServer device with a ContentDirectory service. -} - -@defproc[(media-server-root [server media-server?] - [#:start start exact-nonnegative-integer? 0] - [#:count count exact-nonnegative-integer? 0]) +@defproc[(media-server-root + [server media-server?] + [#:start start exact-nonnegative-integer? 0] + [#:count count exact-nonnegative-integer? 0]) (listof media-entry?)]{ -Returns the direct children of ContentDirectory object @tt{0}. The optional -paging arguments are passed to ContentDirectory. A count of zero asks the -server to return all available children, subject to the server's own limits. + +Browses object @tt{0}, the ContentDirectory root. } -@defproc[(media-server-browse [server media-server?] - [container (or/c string? media-container?) "0"] - [#:start start exact-nonnegative-integer? 0] - [#:count count exact-nonnegative-integer? 0]) +@defproc[(media-server-browse + [server media-server?] + [container (or/c string? media-container?) "0"] + [#:start start exact-nonnegative-integer? 0] + [#:count count exact-nonnegative-integer? 0]) (listof media-entry?)]{ -Returns the direct children of an object identifier or previously returned -container. + +Browses one level below a container ID or a previously returned container. + +In ContentDirectory, @racket[count] equal to zero asks the server to return all +remaining entries. Use a positive count for large containers. } -@defproc[(media-container-children [server media-server?] - [container media-container?] - [#:start start exact-nonnegative-integer? 0] - [#:count count exact-nonnegative-integer? 0]) +@defproc[(media-container-children + [server media-server?] + [container media-container?] + [#:start start exact-nonnegative-integer? 0] + [#:count count exact-nonnegative-integer? 0]) (listof media-entry?)]{ -Convenience form of @racket[media-server-browse] for a container value. + +Equivalent to browsing the supplied container. } @section{Entries} -@defproc[(media-entry? [value any/c]) boolean?]{Recognises a media container or media item.} -@defproc[(media-entry-id [entry media-entry?]) (or/c #f string?)]{Returns the ContentDirectory object identifier.} -@defproc[(media-entry-parent-id [entry media-entry?]) (or/c #f string?)]{Returns the parent object identifier.} -@defproc[(media-entry-title [entry media-entry?]) string?]{Returns the displayed title.} -@defproc[(media-entry-class [entry media-entry?]) (or/c #f string?)]{Returns the original UPnP class, such as @tt{object.item.audioItem.musicTrack}.} -@defproc[(media-entry-restricted? [entry media-entry?]) boolean?]{Reports whether the server marks the object as restricted.} +@defproc[(media-entry? [value any/c]) boolean?] +@defproc[(media-entry-id [entry media-entry?]) (or/c #f string?)] +@defproc[(media-entry-parent-id [entry media-entry?]) (or/c #f string?)] +@defproc[(media-entry-title [entry media-entry?]) string?] +@defproc[(media-entry-class [entry media-entry?]) (or/c #f string?)] +@defproc[(media-entry-restricted? [entry media-entry?]) boolean?] -@defproc[(media-container? [value any/c]) boolean?]{Recognises a browsable container.} -@defproc[(media-container-child-count [container media-container?]) (or/c #f exact-nonnegative-integer?)]{Returns the advertised number of children, when present.} -@defproc[(media-container-searchable? [container media-container?]) boolean?]{Reports whether the container is advertised as searchable.} +@defproc[(media-container? [value any/c]) boolean?] +@defproc[(media-container-child-count [container media-container?]) + (or/c #f exact-nonnegative-integer?)] +@defproc[(media-container-searchable? [container media-container?]) + boolean?] -@defproc[(media-item? [value any/c]) boolean?]{Recognises a media item.} -@defproc[(media-item-creator [item media-item?]) (or/c #f string?)]{Returns the Dublin Core creator.} -@defproc[(media-item-artists [item media-item?]) (listof string?)]{Returns the advertised artists.} -@defproc[(media-item-album [item media-item?]) (or/c #f string?)]{Returns the album title.} -@defproc[(media-item-genres [item media-item?]) (listof string?)]{Returns the advertised genres.} -@defproc[(media-item-date [item media-item?]) (or/c #f string?)]{Returns the advertised date without imposing a date format.} -@defproc[(media-item-album-art-uri [item media-item?]) (or/c #f string?)]{Returns the album-art URI. Relative URIs are resolved against the device-description URL.} -@defproc[(media-item-resources [item media-item?]) (listof media-resource?)]{Returns all playable or retrievable resources. One item may advertise several encodings.} +@defproc[(media-item? [value any/c]) boolean?] +@defproc[(media-item-creator [item media-item?]) (or/c #f string?)] +@defproc[(media-item-artists [item media-item?]) (listof string?)] +@defproc[(media-item-album [item media-item?]) (or/c #f string?)] +@defproc[(media-item-genres [item media-item?]) (listof string?)] +@defproc[(media-item-date [item media-item?]) (or/c #f string?)] +@defproc[(media-item-album-art-uri [item media-item?]) + (or/c #f string?)] +@defproc[(media-item-resources [item media-item?]) + (listof media-resource?)] @section{Resources} -@defproc[(media-resource? [value any/c]) boolean?]{Recognises a DIDL-Lite resource.} -@defproc[(media-resource-uri [resource media-resource?]) string?]{Returns the resource URI, resolved to an absolute URI where possible.} -@defproc[(media-resource-protocol-info [resource media-resource?]) (or/c #f string?)]{Returns the complete UPnP @tt{protocolInfo} value.} -@defproc[(media-resource-content-type [resource media-resource?]) (or/c #f string?)]{Returns the content-format portion of @tt{protocolInfo}, such as @tt{audio/flac}.} -@defproc[(media-resource-size [resource media-resource?]) (or/c #f exact-nonnegative-integer?)]{Returns the size in bytes.} -@defproc[(media-resource-duration [resource media-resource?]) (or/c #f nonnegative-real?)]{Returns the duration in seconds.} -@defproc[(media-resource-bitrate [resource media-resource?]) (or/c #f exact-nonnegative-integer?)]{Returns the advertised bitrate in bytes per second, as defined by DIDL-Lite.} -@defproc[(media-resource-sample-frequency [resource media-resource?]) (or/c #f exact-nonnegative-integer?)]{Returns the sample frequency in hertz.} -@defproc[(media-resource-bits-per-sample [resource media-resource?]) (or/c #f exact-nonnegative-integer?)]{Returns the advertised bits per sample.} -@defproc[(media-resource-channels [resource media-resource?]) (or/c #f exact-nonnegative-integer?)]{Returns the number of audio channels.} -@defproc[(media-resource-resolution [resource media-resource?]) (or/c #f string?)]{Returns an advertised image or video resolution.} - -@section[#:tag "media-server-example"]{Example} - -@racketblock[ -(define synology - (car (query-media-servers #:interface "10.7.3.118"))) - -(define root (media-server-root synology)) -(define music - (findf (lambda (entry) - (and (media-container? entry) - (string=? (media-entry-title entry) "Muziek"))) - root)) - -(for ([entry (in-list (media-container-children synology music))]) - (printf "~a: ~a\n" - (if (media-container? entry) 'container 'item) - (media-entry-title entry))) -] +@defproc[(media-resource? [value any/c]) boolean?] +@defproc[(media-resource-uri [resource media-resource?]) string?] +@defproc[(media-resource-protocol-info [resource media-resource?]) + (or/c #f string?)] +@defproc[(media-resource-content-type [resource media-resource?]) + (or/c #f string?)] +@defproc[(media-resource-size [resource media-resource?]) + (or/c #f exact-nonnegative-integer?)] +@defproc[(media-resource-duration [resource media-resource?]) + (or/c #f number?)] +@defproc[(media-resource-bitrate [resource media-resource?]) + (or/c #f exact-nonnegative-integer?)] +@defproc[(media-resource-sample-frequency [resource media-resource?]) + (or/c #f exact-nonnegative-integer?)] +@defproc[(media-resource-bits-per-sample [resource media-resource?]) + (or/c #f exact-nonnegative-integer?)] +@defproc[(media-resource-channels [resource media-resource?]) + (or/c #f exact-nonnegative-integer?)] +@defproc[(media-resource-resolution [resource media-resource?]) + (or/c #f string?)]