#lang racket/base (require rackunit racket/port racket/string racket/tcp "../main.rkt" "../service.rkt") (define av1 (upnp-service "urn:schemas-upnp-org:service:AVTransport:1" "urn:upnp-org:serviceId:AVTransport" #f #f #f)) (define av3 (upnp-service "urn:schemas-upnp-org:service:AVTransport:3" "urn:upnp-org:serviceId:AVTransport" #f #f #f)) (define renderer (upnp-device "uuid:test-renderer" "urn:schemas-upnp-org:device:MediaRenderer:1" "Test Renderer" "Test Manufacturer" "Test Model" "1000" #f "http://127.0.0.1/device.xml" "127.0.0.1" #f (list av1 av3) '() (hash "x_dlnadoc" "DMR-1.50"))) (check-eq? (upnp-device-kind renderer) 'media-renderer) (check-equal? (upnp-device-name renderer) "Test Renderer") (check-equal? (upnp-device-model renderer) "Test Model (1000)") (check-eq? (upnp-device-service renderer 'av-transport) av3) (check-true (media-renderer? renderer)) (check-true (media-renderer-dlna? renderer)) (check-not-false (assoc 'media-renderer (upnp-device-kinds))) (check-not-false (assoc 'av-transport (upnp-service-kinds))) (check-not-false (assoc 'scan (upnp-service-kinds))) (define listener (tcp-listen 0 4 #t "127.0.0.1")) (define-values (_local-host port _remote-host _remote-port) (tcp-addresses listener #t)) (define recorded-bodies (box '())) (define (read-request in) (let* ([request-line (read-line in 'any)] [headers (let loop ([headers (hash)]) (let ([line (read-line in 'any)]) (cond [(or (eof-object? line) (string=? line "")) headers] [else (let ([match (regexp-match #px"^([^:]+):[ \t]*(.*)$" line)]) (loop (if match (hash-set headers (string-downcase (cadr match)) (caddr match)) headers)))])))] [content-length (string->number (hash-ref headers "content-length" "0"))] [body (if (and content-length (positive? content-length)) (read-string content-length in) "")]) (values request-line headers body))) (define (soap-response action content) (string-append "" "" "" content "")) (define scpd (string-append "" "" "SetAVTransportURI" "Play" "Pause" "GetTransportInfo" "GetPositionInfo" "")) (define fault (string-append "" "" "" "701Transition not available" "")) (define (response-for request-line headers) (let ([action (hash-ref headers "soapaction" "")]) (cond [(regexp-match? #rx"GET /avtransport.xml" request-line) (values 200 scpd)] [(regexp-match? #rx"GetTransportInfo" action) (values 200 (soap-response "GetTransportInfo" "PLAYINGOK1"))] [(regexp-match? #rx"GetPositionInfo" action) (values 200 (soap-response "GetPositionInfo" "200:03:3000:01:15.5http://example/test.flac"))] [(regexp-match? #rx"GetVolume" action) (values 200 (soap-response "GetVolume" "37"))] [(regexp-match? #rx"GetProtocolInfo" action) (values 200 (soap-response "GetProtocolInfo" "http-get:*:audio/flac:*http-get:*:audio/mpeg:*, http-get:*:audio/flac:*"))] [(regexp-match? #rx"GetCurrentConnectionIDs" action) (values 200 (soap-response "GetCurrentConnectionIDs" "0,2"))] [(regexp-match? #rx"Browse" action) (values 200 (soap-response "Browse" "<DIDL-Lite/>149"))] [(regexp-match? #rx"FaultAction" action) (values 500 fault)] [else (let ([match (regexp-match #px"#([^\"]+)\"?$" action)]) (values 200 (soap-response (if match (cadr match) "Action") "")))]))) (define server (thread (lambda () (for ([request-number (in-range 10)]) (let-values ([(in out) (tcp-accept listener)]) (let-values ([(request-line headers body) (read-request in)]) (set-box! recorded-bodies (cons body (unbox recorded-bodies))) (let-values ([(status response) (response-for request-line headers)]) (let ([response-bytes (string->bytes/utf-8 response)]) (fprintf out "HTTP/1.1 ~a ~a\r\nContent-Type: text/xml\r\nContent-Length: ~a\r\nConnection: close\r\n\r\n" status (if (= status 200) "OK" "Internal Server Error") (bytes-length response-bytes)) (write-bytes response-bytes out) (flush-output out)))) (close-input-port in) (close-output-port out)))))) (define control-url (format "http://127.0.0.1:~a/control" port)) (define scpd-url (format "http://127.0.0.1:~a/avtransport.xml" port)) (define av (upnp-service "urn:schemas-upnp-org:service:AVTransport:1" "urn:upnp-org:serviceId:AVTransport" scpd-url control-url #f)) (define rendering (upnp-service "urn:schemas-upnp-org:service:RenderingControl:1" "urn:upnp-org:serviceId:RenderingControl" #f control-url #f)) (define connection (upnp-service "urn:schemas-upnp-org:service:ConnectionManager:1" "urn:upnp-org:serviceId:ConnectionManager" #f control-url #f)) (define directory (upnp-service "urn:schemas-upnp-org:service:ContentDirectory:1" "urn:upnp-org:serviceId:ContentDirectory" #f control-url #f)) (check-equal? (upnp-service-actions av) '("SetAVTransportURI" "Play" "Pause" "GetTransportInfo" "GetPositionInfo")) (check-true (upnp-service-supports-action? av 'play)) (check-eq? (av-transport-status av) 'playing) (let ([position (av-transport-position av)]) (check-equal? (transport-position-track position) 2) (check-equal? (transport-position-seconds position) 151/2) (check-equal? (transport-position-duration position) 210) (check-equal? (transport-position-uri position) "http://example/test.flac")) (av-transport-set-uri! av "http://127.0.0.1/test?a=1&b=2") (check-equal? (rendering-control-volume rendering) 37) (rendering-control-set-volume! rendering 300) (let-values ([(source sink) (connection-manager-protocols connection)]) (check-equal? source '("http-get:*:audio/flac:*")) (check-equal? sink '("http-get:*:audio/mpeg:*" "http-get:*:audio/flac:*"))) (check-equal? (connection-manager-connection-ids connection) '(0 2)) (let ([result (content-directory-browse directory "0")]) (check-equal? (content-result-content result) "") (check-equal? (content-result-number-returned result) 1) (check-equal? (content-result-total-matches result) 4) (check-equal? (content-result-update-id result) 9)) (check-exn (lambda (exception) (and (exn:fail:upnp? exception) (equal? (exn:fail:upnp-code exception) "701") (equal? (exn:fail:upnp-description exception) "Transition not available"))) (lambda () (upnp-service-call av "FaultAction"))) (thread-wait server) (tcp-close listener) (check-true (ormap (lambda (body) (regexp-match? #rx"http://127.0.0.1/test\\?a=1&b=2" body)) (unbox recorded-bodies))) (displayln "UPnP tests passed")