Skip to content

Commit da33000

Browse files
committed
update codebase
1 parent 80d4919 commit da33000

12 files changed

Lines changed: 279 additions & 198 deletions

File tree

httphandlers/httphandlers.go

Lines changed: 11 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ func (s *HTTPserver) ServeMediaHandler() http.HandlerFunc {
278278
if strings.HasSuffix(requestPathLower, ".m3u8") {
279279
w.Header().Set("Content-Type", "application/vnd.apple.mpegurl")
280280
} else if strings.HasSuffix(requestPathLower, ".ts") {
281-
w.Header().Set("Content-Type", "video/mp2t")
281+
w.Header().Set("Content-Type", "video/mpeg")
282282
} else if strings.HasSuffix(requestPathLower, ".mp4") || strings.HasSuffix(requestPathLower, ".m4s") {
283283
w.Header().Set("Content-Type", "video/mp4")
284284
}
@@ -410,7 +410,7 @@ func (s *HTTPserver) AddHLSHandler(urlPrefix, dir string) {
410410
w.Header().Set("Pragma", "no-cache")
411411
w.Header().Set("Expires", "0")
412412
} else if strings.HasSuffix(requestPathLower, ".ts") {
413-
w.Header().Set("Content-Type", "video/MP2T")
413+
w.Header().Set("Content-Type", "video/mpeg")
414414
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
415415
w.Header().Set("Pragma", "no-cache")
416416
w.Header().Set("Expires", "0")
@@ -458,7 +458,7 @@ func serveContent(w http.ResponseWriter, r *http.Request, tv *soapcalls.TVPayloa
458458
if tv != nil {
459459
isMedia = true
460460
transcode = tv.Transcode
461-
mediaType = tv.MediaType
461+
mediaType = utils.DLNAResourceMediaType(tv.MediaType, transcode)
462462
seek = tv.Seekable
463463
tv.Log().Debug("", "Method", "DLNAMediaHTTP", "Action", "Request", "HTTP Method", r.Method, "Path", r.URL.Path,
464464
"TimeSeekRange", r.Header.Get("TimeSeekRange.dlna.org"), "Range", r.Header.Get("Range"),
@@ -473,10 +473,9 @@ func serveContent(w http.ResponseWriter, r *http.Request, tv *soapcalls.TVPayloa
473473
mediaType = "video/mp4" // Chromecast transcoding outputs fragmented MP4
474474
}
475475

476-
w.Header()["transferMode.dlna.org"] = []string{"Interactive"}
476+
w.Header()["transferMode.dlna.org"] = []string{utils.DLNATransferMode(mediaType)}
477477

478478
if isMedia {
479-
w.Header()["transferMode.dlna.org"] = []string{"Streaming"}
480479
w.Header()["realTimeInfo.dlna.org"] = []string{"DLNA.ORG_TLAG=*"}
481480
w.Header()["Content-Type"] = []string{mediaType}
482481
}
@@ -527,13 +526,7 @@ func serveContentBytes(w http.ResponseWriter, r *http.Request, mediaType string,
527526
}
528527

529528
if r.Header.Get("getcontentFeatures.dlna.org") == "1" {
530-
contentFeatures, err := utils.BuildContentFeatures(mediaType, "01", false)
531-
if err != nil {
532-
http.NotFound(w, r)
533-
return
534-
}
535-
536-
w.Header()["contentFeatures.dlna.org"] = []string{contentFeatures}
529+
w.Header()["contentFeatures.dlna.org"] = []string{utils.BuildDLNAContentFeatures(utils.DLNAContentFeaturesOptions{ByteSeek: true})}
537530
}
538531

539532
bReader := bytes.NewReader(f)
@@ -545,13 +538,7 @@ func serveContentReadClose(w http.ResponseWriter, r *http.Request, tv *soapcalls
545538
defer f.Close()
546539

547540
if r.Header.Get("getcontentFeatures.dlna.org") == "1" {
548-
contentFeatures, err := utils.BuildContentFeatures(mediaType, "00", transcode)
549-
if err != nil {
550-
http.NotFound(w, r)
551-
return
552-
}
553-
554-
w.Header()["contentFeatures.dlna.org"] = []string{contentFeatures}
541+
w.Header()["contentFeatures.dlna.org"] = []string{utils.BuildDLNAContentFeatures(utils.DLNAContentFeaturesOptions{Converted: transcode})}
555542
}
556543
// In ffmpeg we can emulate seek support for live streams
557544
if transcode && r.Method == http.MethodGet && strings.Contains(mediaType, "video") {
@@ -608,18 +595,7 @@ func serveContentSeekCloser(w http.ResponseWriter, r *http.Request, mediaType st
608595
defer f.Close()
609596

610597
if r.Header.Get("getcontentFeatures.dlna.org") == "1" {
611-
seekflag := "00"
612-
if seek {
613-
seekflag = "01"
614-
}
615-
616-
contentFeatures, err := utils.BuildContentFeatures(mediaType, seekflag, false)
617-
if err != nil {
618-
http.NotFound(w, r)
619-
return
620-
}
621-
622-
w.Header()["contentFeatures.dlna.org"] = []string{contentFeatures}
598+
w.Header()["contentFeatures.dlna.org"] = []string{utils.BuildDLNAContentFeatures(utils.DLNAContentFeaturesOptions{ByteSeek: seek})}
623599
}
624600

625601
name := strings.TrimLeft(r.URL.Path, "/")
@@ -628,18 +604,10 @@ func serveContentSeekCloser(w http.ResponseWriter, r *http.Request, mediaType st
628604

629605
func serveContentCustomType(w http.ResponseWriter, r *http.Request, tv *soapcalls.TVPayload, tcOpts *utils.TranscodeOptions, mediaType string, transcode, seek bool, f osFileType, ff *exec.Cmd) {
630606
if r.Header.Get("getcontentFeatures.dlna.org") == "1" {
631-
seekflag := "00"
632-
if seek {
633-
seekflag = "01"
634-
}
635-
636-
contentFeatures, err := utils.BuildContentFeatures(mediaType, seekflag, transcode)
637-
if err != nil {
638-
http.NotFound(w, r)
639-
return
640-
}
641-
642-
w.Header()["contentFeatures.dlna.org"] = []string{contentFeatures}
607+
w.Header()["contentFeatures.dlna.org"] = []string{utils.BuildDLNAContentFeatures(utils.DLNAContentFeaturesOptions{
608+
ByteSeek: seek && !transcode,
609+
Converted: transcode,
610+
})}
643611
}
644612
if transcode && r.Method == http.MethodGet && strings.Contains(mediaType, "video") {
645613
// Since we're dealing with an io.Reader we can't

httphandlers/httphandlers_test.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ func TestServeContent(t *testing.T) {
8787

8888
for c := range strings.SplitSeq(cf[0], ";") {
8989
if strings.Contains(c, "DLNA.ORG_OP") {
90-
if tc.tvdata != nil && tc.tvdata.Transcode && c != "DLNA.ORG_OP=00" {
91-
t.Fatalf("%s: no proper DLNA.ORG_OP header for transcoded video", tc.name)
90+
if tc.tvdata != nil && tc.tvdata.Transcode {
91+
t.Fatalf("%s: transcoded media must not advertise seek", tc.name)
9292
}
9393
}
9494
}
@@ -133,21 +133,39 @@ func TestTranscodedDLNAHEADKeepsPlayableResourceContract(t *testing.T) {
133133
if got := result.Header.Get("Content-Length"); got != strconv.Itoa(len(media)) {
134134
t.Fatalf("content length = %q", got)
135135
}
136-
if got := result.Header.Get("Content-Type"); got != "video/mp4" {
136+
if got := result.Header.Get("Content-Type"); got != "video/mpeg" {
137137
t.Fatalf("content type = %q", got)
138138
}
139139
if got := result.Header.Get("Accept-Ranges"); got != "" {
140140
t.Fatalf("accept ranges = %q", got)
141141
}
142142
features := result.Header["contentFeatures.dlna.org"] //nolint:staticcheck
143-
if len(features) != 1 || !strings.Contains(features[0], "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5") || !strings.Contains(features[0], "DLNA.ORG_OP=00") || !strings.Contains(features[0], "DLNA.ORG_CI=1") || !strings.Contains(features[0], "DLNA.ORG_FLAGS=01700000") {
143+
if len(features) != 1 || features[0] != "DLNA.ORG_CI=1" {
144144
t.Fatalf("content features = %q", features)
145145
}
146146
if strings.Contains(features[0], "DLNA.ORG_OP=10") {
147147
t.Fatalf("unsupported live time seek advertised: %q", features)
148148
}
149149
}
150150

151+
func TestDLNAImageUsesInteractiveTransferMode(t *testing.T) {
152+
tv := &soapcalls.TVPayload{MediaType: "image/jpeg", Seekable: true}
153+
request := httptest.NewRequest(http.MethodGet, "/photo.jpg", nil)
154+
request.Header.Set("getcontentFeatures.dlna.org", "1")
155+
response := httptest.NewRecorder()
156+
157+
serveContent(response, request, tv, nil, []byte("jpeg"), new(exec.Cmd))
158+
if got := response.Header().Get("Content-Type"); got != "image/jpeg" {
159+
t.Fatalf("content type = %q", got)
160+
}
161+
if got := response.Header()["transferMode.dlna.org"]; len(got) != 1 || got[0] != "Interactive" { //nolint:staticcheck
162+
t.Fatalf("transfer mode = %q, want Interactive", got)
163+
}
164+
if got := response.Header()["contentFeatures.dlna.org"]; len(got) != 1 || got[0] != "DLNA.ORG_OP=01;DLNA.ORG_CI=0" { //nolint:staticcheck
165+
t.Fatalf("content features = %q", got)
166+
}
167+
}
168+
151169
func TestStaticHandlerMethodsMIMEAndCORS(t *testing.T) {
152170
payload := []byte("normalized artwork")
153171
srv := NewServer("127.0.0.1:0")
@@ -300,7 +318,7 @@ func (t *testReadSeekCloser) Close() error {
300318
// concurrent GET must be refused outright rather than served a body made of
301319
// whichever packets it managed to steal from the renderer already playing.
302320
func TestLiveStreamServesOneReaderAtATime(t *testing.T) {
303-
tv := &soapcalls.TVPayload{MediaType: "video/mp2t"}
321+
tv := &soapcalls.TVPayload{MediaType: "video/mpeg"}
304322

305323
var (
306324
mu sync.Mutex

internal/gui/dlna_screencast.go

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,9 @@ import (
1111
"go2tv.app/screencast/ts"
1212
)
1313

14-
// dlnaScreencastMediaType is the DLNA media type used for the live MPEG-TS
15-
// stream. The first path segment ("video") matches the Sink protocol list
16-
// advertised by most renderers (see parseProtocolInfo, which compares only
17-
// that segment). The pipeline itself is the shared go2tv.app/screencast/ts
18-
// package, which muxes plain 188-byte TS packets, so this pairs with the _ISO
19-
// profile in dlnaprofiles rather than the 192-byte m2ts one.
20-
//
21-
// video/vnd.dlna.mpeg-tts, the type DLNA pairs with the m2ts profiles, is
22-
// deliberately not used: ffmpeg's m2ts mode declares the AAC track as PES
23-
// private data, which GStreamer-based renderers drop, leaving a silent
24-
// picture. See the screencast/ts package comment.
25-
const dlnaScreencastMediaType = "video/mp2t"
14+
// dlnaScreencastMediaType describes the plain 188-byte MPEG-TS stream emitted
15+
// by the screencast pipeline. DLNA uses video/mpeg for this wire format.
16+
const dlnaScreencastMediaType = "video/mpeg"
2617

2718
// errScreencastStreamBusy is returned when the live stream is already being
2819
// served to a renderer.
@@ -31,11 +22,7 @@ var errScreencastStreamBusy = errors.New("screencast stream already has a reader
3122
// startDLNAScreencast starts the shared MPEG-TS pipeline. *ts.Session already
3223
// satisfies screencastSession, so it is handed to the caller as is.
3324
//
34-
// Audio is always captured. It is not a preference: we advertise the
35-
// AVC_TS_MP_HD_AAC_MULT5_ISO profile, which promises the renderer an AAC track,
36-
// so a video-only stream would make that promise false. Nothing is lost by
37-
// forcing it either, because the pipeline substitutes synthetic silence when
38-
// the machine has no audio source, so an AAC track exists either way.
25+
// Audio is always captured so the renderer receives a complete stream.
3926
func startDLNAScreencast(ffmpegPath string, logOutput io.Writer) (*ts.Session, error) {
4027
return ts.Start(&ts.Options{
4128
FFmpegPath: ffmpegPath,

internal/mediaserver/server.go

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -418,25 +418,23 @@ func (s *Server) serveHTTP(w http.ResponseWriter, request *http.Request) {
418418
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
419419
return
420420
}
421-
if r.mediaType != "" {
422-
w.Header().Set("Content-Type", r.mediaType)
423-
w.Header().Set("transferMode.dlna.org", "Streaming")
421+
wireMediaType := r.mediaType
422+
if r.request.Target.Protocol != "Chromecast" {
423+
wireMediaType = utils.DLNAResourceMediaType(r.mediaType, r.request.Transcode)
424+
}
425+
if wireMediaType != "" {
426+
w.Header().Set("Content-Type", wireMediaType)
427+
w.Header().Set("transferMode.dlna.org", utils.DLNATransferMode(wireMediaType))
424428
w.Header().Set("realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*")
425429
}
426430
if r.request.Target.Protocol == "Chromecast" {
427431
w.Header().Set("Access-Control-Allow-Origin", "*")
428432
}
429433
if request.Header.Get("getcontentFeatures.dlna.org") == "1" {
430-
seek := "00"
431-
if !r.request.Transcode && r.open != nil {
432-
seek = "01"
433-
}
434-
contentFeatures, err := utils.BuildContentFeatures(r.mediaType, seek, r.request.Transcode)
435-
if err != nil {
436-
http.NotFound(w, request)
437-
return
438-
}
439-
w.Header().Set("contentFeatures.dlna.org", contentFeatures)
434+
w.Header().Set("contentFeatures.dlna.org", utils.BuildDLNAContentFeatures(utils.DLNAContentFeaturesOptions{
435+
ByteSeek: !r.request.Transcode && (r.open != nil || r.contents != nil),
436+
Converted: r.request.Transcode,
437+
}))
440438
}
441439
if r.artwork {
442440
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")

internal/mediaserver/server_test.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,31 @@ func TestGETHEADMethodRangeAndCallbackCoexist(t *testing.T) {
287287
}
288288
}
289289

290+
func TestImageRouteUsesInteractiveTransferMode(t *testing.T) {
291+
server := New(Config{ListenAddr: "127.0.0.1:0"})
292+
startTestServer(t, server, mediaRequest([]byte("media"), ".mp4", "video/mp4"))
293+
route, err := server.Add(context.Background(), playback.RouteRequest{MediaType: "image/jpeg", Contents: []byte("jpeg")})
294+
if err != nil {
295+
t.Fatal(err)
296+
}
297+
request, _ := http.NewRequest(http.MethodGet, route.URL, nil)
298+
request.Header.Set("getcontentFeatures.dlna.org", "1")
299+
response, err := http.DefaultClient.Do(request)
300+
if err != nil {
301+
t.Fatal(err)
302+
}
303+
response.Body.Close()
304+
if got := response.Header.Get("Content-Type"); got != "image/jpeg" {
305+
t.Fatalf("content type = %q", got)
306+
}
307+
if got := response.Header.Get("transferMode.dlna.org"); got != "Interactive" {
308+
t.Fatalf("transfer mode = %q, want Interactive", got)
309+
}
310+
if got := response.Header.Get("contentFeatures.dlna.org"); got != "DLNA.ORG_OP=01;DLNA.ORG_CI=0" {
311+
t.Fatalf("content features = %q", got)
312+
}
313+
}
314+
290315
func TestTranscodedDLNAKeepsPlayableResourceContract(t *testing.T) {
291316
requests := make(chan playback.ServerRequest, 1)
292317
server := New(Config{
@@ -319,13 +344,13 @@ func TestTranscodedDLNAKeepsPlayableResourceContract(t *testing.T) {
319344
if response.StatusCode != http.StatusOK {
320345
t.Fatalf("HEAD status=%d", response.StatusCode)
321346
}
322-
if features := response.Header.Get("contentFeatures.dlna.org"); !strings.Contains(features, "DLNA.ORG_OP=00") || !strings.Contains(features, "DLNA.ORG_CI=1") {
347+
if features := response.Header.Get("contentFeatures.dlna.org"); features != "DLNA.ORG_CI=1" {
323348
t.Fatalf("content features = %q", features)
324349
}
325-
if got := response.Header.Get("Content-Type"); got != "video/x-matroska" {
350+
if got := response.Header.Get("Content-Type"); got != "video/mpeg" {
326351
t.Fatalf("content type = %q", got)
327352
}
328-
if features := response.Header.Get("contentFeatures.dlna.org"); !strings.Contains(features, "DLNA.ORG_PN=MATROSKA") || !strings.Contains(features, "DLNA.ORG_FLAGS=01700000") || strings.Contains(features, "DLNA.ORG_OP=10") {
353+
if features := response.Header.Get("contentFeatures.dlna.org"); strings.Contains(features, "DLNA.ORG_OP") || strings.Contains(features, "DLNA.ORG_FLAGS") {
329354
t.Fatalf("transcode profile = %q", features)
330355
}
331356
if got := response.Header.Get("TimeSeekRange.dlna.org"); got != "" {

internal/playbackadapter/adapters_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,6 @@ func TestDLNAGaplessQueuesAndReadsNextURI(t *testing.T) {
424424
}
425425
if !strings.Contains(setNextBody, "<NextURI>http://127.0.0.1/next.mp3</NextURI>") ||
426426
!strings.Contains(setNextBody, "Next track") ||
427-
!strings.Contains(setNextBody, "DLNA.ORG_OP=00") ||
428427
!strings.Contains(setNextBody, "DLNA.ORG_CI=1") ||
429428
!strings.Contains(setNextBody, "duration=&#34;00:01:40&#34;") {
430429
t.Fatalf("SetNextAVTransportURI body = %q", setNextBody)

soapcalls/soapbuilders.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -443,18 +443,14 @@ func setNextAVTransportSoapBuild(tvdata *TVPayload, clear bool) ([]byte, error)
443443
}
444444

445445
func buildDIDLLite(tvdata *TVPayload, mediaURL string, mediaMetadata metadata.Media) ([]byte, error) {
446-
seekflag := "00"
447-
if tvdata.Seekable {
448-
seekflag = "01"
449-
}
450-
451-
contentFeatures, err := utils.BuildContentFeatures(tvdata.MediaType, seekflag, tvdata.Transcode)
452-
if err != nil {
453-
return nil, fmt.Errorf("build content features: %w", err)
454-
}
446+
wireMediaType := utils.DLNAResourceMediaType(tvdata.MediaType, tvdata.Transcode)
447+
contentFeatures := utils.BuildDLNAContentFeatures(utils.DLNAContentFeaturesOptions{
448+
ByteSeek: tvdata.Seekable && !tvdata.Transcode,
449+
Converted: tvdata.Transcode,
450+
})
455451

456452
class := "object.item.videoItem.movie"
457-
switch strings.Split(tvdata.MediaType, "/")[0] {
453+
switch strings.Split(wireMediaType, "/")[0] {
458454
case "audio":
459455
class = "object.item.audioItem.musicTrack"
460456
case "image":
@@ -472,7 +468,7 @@ func buildDIDLLite(tvdata *TVPayload, mediaURL string, mediaMetadata metadata.Me
472468
}
473469

474470
resNodeData := []resNode{{
475-
ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", tvdata.MediaType, contentFeatures),
471+
ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", wireMediaType, contentFeatures),
476472
Value: mediaURL,
477473
}}
478474
if tvdata.MediaDuration > 0 {

soapcalls/soapbuilders_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ func TestBuildDIDLLiteTranscodedKeepsPlayableResourceContract(t *testing.T) {
133133
t.Fatal(err)
134134
}
135135
metadata := string(got)
136-
for _, want := range []string{"http-get:*:video/mp4:", "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5", "DLNA.ORG_OP=00", "DLNA.ORG_CI=1", "DLNA.ORG_FLAGS=01700000", `duration="01:42:56"`, "http://host/movie.mp4"} {
136+
for _, want := range []string{"http-get:*:video/mpeg:DLNA.ORG_CI=1", "DLNA.ORG_CI=1", `duration="01:42:56"`, "http://host/movie.mp4"} {
137137
if !strings.Contains(metadata, want) {
138138
t.Fatalf("DIDL missing %q: %s", want, metadata)
139139
}

0 commit comments

Comments
 (0)