Media file server

This commit is contained in:
2026-07-15 20:48:58 +02:00
parent c6954f5109
commit 1884008dd6
5 changed files with 349 additions and 138 deletions
+35
View File
@@ -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)
+4 -10
View File
@@ -1,23 +1,17 @@
#lang racket/base #lang racket/base
;; Main public interface for racket-upnp. ;; 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/<service>
(require "query.rkt" (require "query.rkt"
"service.rkt" "service.rkt"
"media-renderer.rkt" "media-renderer.rkt"
"media-server.rkt") "media-server.rkt"
"media-file-server.rkt")
(provide (provide
(all-from-out (all-from-out
"query.rkt" "query.rkt"
"service.rkt" "service.rkt"
"media-renderer.rkt" "media-renderer.rkt"
"media-server.rkt")) "media-server.rkt"
"media-file-server.rkt"))
+113
View File
@@ -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.
+102 -39
View File
@@ -1,51 +1,114 @@
#lang scribble/manual #lang scribble/manual
@(require (for-label racket/base @(require
racket/contract (for-label
(file "../main.rkt") racket/base
(file "../media-renderer.rkt") (file "../media-renderer.rkt")))
(file "../services/av-transport.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. This module provides a device-oriented interface for UPnP and DLNA media
The returned renderer remains an @racket[upnp-device?], so generic service renderers. It hides the AVTransport and RenderingControl services used for
inspection is still available. common playback and volume operations.
@defproc[(query-media-renderers [#:interface interface (or/c #f string?) #f] @defproc[(query-media-renderers
[#:dns? dns? boolean? #f] [#:interface interface (or/c #f string?) #f]
[#:dlna-only? dlna-only? boolean? #f]) [#:dns? dns? boolean? #f]
(listof upnp-device?)]{ [#:dlna-only? dlna-only? boolean? #f])
Discovers UPnP MediaRenderers. When @racket[dlna-only?] is true, only devices (listof media-renderer?)]{
advertising an @tt{X_DLNADOC} value are returned.
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[(get-media-renderer [filter-name string?])
@defproc[(media-renderer-dlna? [renderer media-renderer?]) boolean?]{Reports whether the device description advertises a DLNA document identifier.} (or/c #f media-renderer?)]{
@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.}
@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[ @defproc[(media-renderer? [value any/c]) boolean?]
(define renderer @defproc[(media-renderer-name [renderer media-renderer?]) string?]
(car (query-media-renderers #:interface "10.7.3.118"))) @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! @section{Playback}
renderer
"http://10.7.3.32:50002/music/track.flac")
(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]
+95 -89
View File
@@ -1,114 +1,120 @@
#lang scribble/manual #lang scribble/manual
@(require (for-label racket/base @(require
racket/contract (for-label
racket/list racket/base
(file "../main.rkt") (file "../media-server.rkt")))
(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 This module provides a higher-level browser for UPnP and DLNA media servers.
@racketmodname[racket-upnp/services/content-directory] into ordinary Racket It converts ContentDirectory DIDL-Lite XML into containers, items, and media
values. Containers are browsed one level at a time; the module does not resources.
recursively load an entire media library. This is suitable for a browser that
requests children when the user expands a container.
The hierarchy is the logical hierarchy published by the media server. A server @defproc[(query-media-servers
may offer views such as artist, album, genre, and physical folder, so a track [#:interface interface (or/c #f string?) #f]
can occur in more than one container. [#: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} @section{Browsing}
@defproc[(query-media-servers [#:interface interface (or/c #f string?) #f] @defproc[(media-server-root
[#:dns? dns? boolean? #f]) [server media-server?]
(listof media-server?)]{ [#:start start exact-nonnegative-integer? 0]
Discovers UPnP MediaServer devices that provide a ContentDirectory service. [#:count count exact-nonnegative-integer? 0])
}
@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])
(listof media-entry?)]{ (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 Browses object @tt{0}, the ContentDirectory root.
server to return all available children, subject to the server's own limits.
} }
@defproc[(media-server-browse [server media-server?] @defproc[(media-server-browse
[container (or/c string? media-container?) "0"] [server media-server?]
[#:start start exact-nonnegative-integer? 0] [container (or/c string? media-container?) "0"]
[#:count count exact-nonnegative-integer? 0]) [#:start start exact-nonnegative-integer? 0]
[#:count count exact-nonnegative-integer? 0])
(listof media-entry?)]{ (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?] @defproc[(media-container-children
[container media-container?] [server media-server?]
[#:start start exact-nonnegative-integer? 0] [container media-container?]
[#:count count exact-nonnegative-integer? 0]) [#:start start exact-nonnegative-integer? 0]
[#:count count exact-nonnegative-integer? 0])
(listof media-entry?)]{ (listof media-entry?)]{
Convenience form of @racket[media-server-browse] for a container value.
Equivalent to browsing the supplied container.
} }
@section{Entries} @section{Entries}
@defproc[(media-entry? [value any/c]) boolean?]{Recognises a media container or media item.} @defproc[(media-entry? [value any/c]) boolean?]
@defproc[(media-entry-id [entry media-entry?]) (or/c #f string?)]{Returns the ContentDirectory object identifier.} @defproc[(media-entry-id [entry media-entry?]) (or/c #f string?)]
@defproc[(media-entry-parent-id [entry media-entry?]) (or/c #f string?)]{Returns the parent object identifier.} @defproc[(media-entry-parent-id [entry media-entry?]) (or/c #f string?)]
@defproc[(media-entry-title [entry media-entry?]) string?]{Returns the displayed title.} @defproc[(media-entry-title [entry media-entry?]) string?]
@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-class [entry media-entry?]) (or/c #f string?)]
@defproc[(media-entry-restricted? [entry media-entry?]) boolean?]{Reports whether the server marks the object as restricted.} @defproc[(media-entry-restricted? [entry media-entry?]) boolean?]
@defproc[(media-container? [value any/c]) boolean?]{Recognises a browsable container.} @defproc[(media-container? [value any/c]) boolean?]
@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-child-count [container media-container?])
@defproc[(media-container-searchable? [container media-container?]) boolean?]{Reports whether the container is advertised as searchable.} (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? [value any/c]) boolean?]
@defproc[(media-item-creator [item media-item?]) (or/c #f string?)]{Returns the Dublin Core creator.} @defproc[(media-item-creator [item media-item?]) (or/c #f string?)]
@defproc[(media-item-artists [item media-item?]) (listof string?)]{Returns the advertised artists.} @defproc[(media-item-artists [item media-item?]) (listof string?)]
@defproc[(media-item-album [item media-item?]) (or/c #f string?)]{Returns the album title.} @defproc[(media-item-album [item media-item?]) (or/c #f string?)]
@defproc[(media-item-genres [item media-item?]) (listof string?)]{Returns the advertised genres.} @defproc[(media-item-genres [item media-item?]) (listof string?)]
@defproc[(media-item-date [item media-item?]) (or/c #f string?)]{Returns the advertised date without imposing a date format.} @defproc[(media-item-date [item media-item?]) (or/c #f string?)]
@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-album-art-uri [item media-item?])
@defproc[(media-item-resources [item media-item?]) (listof media-resource?)]{Returns all playable or retrievable resources. One item may advertise several encodings.} (or/c #f string?)]
@defproc[(media-item-resources [item media-item?])
(listof media-resource?)]
@section{Resources} @section{Resources}
@defproc[(media-resource? [value any/c]) boolean?]{Recognises a DIDL-Lite resource.} @defproc[(media-resource? [value any/c]) boolean?]
@defproc[(media-resource-uri [resource media-resource?]) string?]{Returns the resource URI, resolved to an absolute URI where possible.} @defproc[(media-resource-uri [resource media-resource?]) string?]
@defproc[(media-resource-protocol-info [resource media-resource?]) (or/c #f string?)]{Returns the complete UPnP @tt{protocolInfo} value.} @defproc[(media-resource-protocol-info [resource media-resource?])
@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}.} (or/c #f string?)]
@defproc[(media-resource-size [resource media-resource?]) (or/c #f exact-nonnegative-integer?)]{Returns the size in bytes.} @defproc[(media-resource-content-type [resource media-resource?])
@defproc[(media-resource-duration [resource media-resource?]) (or/c #f nonnegative-real?)]{Returns the duration in seconds.} (or/c #f string?)]
@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-size [resource media-resource?])
@defproc[(media-resource-sample-frequency [resource media-resource?]) (or/c #f exact-nonnegative-integer?)]{Returns the sample frequency in hertz.} (or/c #f exact-nonnegative-integer?)]
@defproc[(media-resource-bits-per-sample [resource media-resource?]) (or/c #f exact-nonnegative-integer?)]{Returns the advertised bits per sample.} @defproc[(media-resource-duration [resource media-resource?])
@defproc[(media-resource-channels [resource media-resource?]) (or/c #f exact-nonnegative-integer?)]{Returns the number of audio channels.} (or/c #f number?)]
@defproc[(media-resource-resolution [resource media-resource?]) (or/c #f string?)]{Returns an advertised image or video resolution.} @defproc[(media-resource-bitrate [resource media-resource?])
(or/c #f exact-nonnegative-integer?)]
@section[#:tag "media-server-example"]{Example} @defproc[(media-resource-sample-frequency [resource media-resource?])
(or/c #f exact-nonnegative-integer?)]
@racketblock[ @defproc[(media-resource-bits-per-sample [resource media-resource?])
(define synology (or/c #f exact-nonnegative-integer?)]
(car (query-media-servers #:interface "10.7.3.118"))) @defproc[(media-resource-channels [resource media-resource?])
(or/c #f exact-nonnegative-integer?)]
(define root (media-server-root synology)) @defproc[(media-resource-resolution [resource media-resource?])
(define music (or/c #f string?)]
(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)))
]