Skip to content

Commit 9c4ad52

Browse files
authored
Merge pull request #169 from alexballas/devel
v2.6.1
2 parents cc0c1fb + d0795fa commit 9c4ad52

7 files changed

Lines changed: 79 additions & 36 deletions

File tree

assets/linux/app.go2tv.go2tv.appdata.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@
5151
</branding>
5252

5353
<releases>
54+
<release version="2.6.1" date="2026-09-16" type="stable">
55+
<description>
56+
<ul>
57+
<li>Fixed a `getprotocolinfo` call error in version 2.6.0 that caused many MKV files to fail to play.</li>
58+
</ul>
59+
</description>
60+
<url type="details">https://github.com/alexballas/go2tv/releases/tag/v2.6.1</url>
61+
</release>
5462
<release version="2.6.0" date="2026-09-14" type="stable">
5563
<description>
5664
<ul>

internal/gui/main.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import (
66
"context"
77
"errors"
88
"fmt"
9+
"image/color"
910
"net/url"
1011
"sort"
1112
"time"
1213

1314
ttwidget "github.com/alexballas/fyne-tooltip/widget"
1415
"github.com/alexballas/refyne/v2"
16+
"github.com/alexballas/refyne/v2/canvas"
1517
"github.com/alexballas/refyne/v2/container"
1618
"github.com/alexballas/refyne/v2/data/binding"
1719
"github.com/alexballas/refyne/v2/lang"
@@ -183,6 +185,17 @@ func mainWindow(s *FyneScreen) fyne.CanvasObject {
183185
})
184186
playpause.Importance = widget.HighImportance
185187
// playpause.Alignment = widget.ButtonAlignCenter
188+
// Reserve the widest playback label so transport controls stay in place.
189+
playText := playpause.Text
190+
playSize := playpause.MinSize()
191+
for _, label := range []string{lang.L("Cast"), lang.L("Play"), lang.L("Pause")} {
192+
playpause.SetText(label + " ")
193+
playSize = playSize.Max(playpause.MinSize())
194+
}
195+
playpause.SetText(playText)
196+
playSpace := canvas.NewRectangle(color.Transparent)
197+
playSpace.SetMinSize(playSize)
198+
playControl := container.NewStack(playSpace, playpause)
186199

187200
stop := widget.NewButtonWithIcon(lang.L("Stop"), theme.MediaStopIcon(), func() {
188201
stopAction(s)
@@ -362,7 +375,7 @@ func mainWindow(s *FyneScreen) fyne.CanvasObject {
362375
muteSize = muteSize.Max(muteunmute.MinSize())
363376
muteControl := container.NewGridWrap(muteSize, muteunmute)
364377
volumeRow := container.NewHBox(widget.NewLabel(lang.L("Volume")), volumedown, volumeup, muteControl)
365-
transportRow := container.NewHBox(playpause, stop, skipPrevious, skipNext)
378+
transportRow := container.NewHBox(playControl, stop, skipPrevious, skipNext)
366379
actionButtons := container.New(layout.NewCustomPaddedLayout(0, 0, theme.InnerPadding(), 0), container.NewBorder(nil, nil, transportRow, volumeRow))
367380
s.playbackStatus = newPlaybackStatusLabel(lang.L("Select a device"))
368381
s.playbackStatus.Importance = widget.MediumImportance

internal/gui/queue.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,7 @@ type queueRow struct {
680680
thumbPath string
681681
pendingThumbPath string
682682
thumbnailRequestID uint64
683+
thumbnailLoader func(string, mediamodel.MediaKind) *canvas.Image
683684
thumbnail *canvas.Image
684685
fallbackIcon *canvas.Image
685686
title *widget.Label
@@ -707,11 +708,12 @@ func newQueueRow(screen *FyneScreen) *queueRow {
707708
)
708709

709710
row := &queueRow{
710-
screen: screen,
711-
thumbnail: thumbnail,
712-
fallbackIcon: fallbackIcon,
713-
title: title,
714-
currentIcon: widget.NewIcon(nil),
711+
screen: screen,
712+
thumbnailLoader: screen.queueMediaThumbnail,
713+
thumbnail: thumbnail,
714+
fallbackIcon: fallbackIcon,
715+
title: title,
716+
currentIcon: widget.NewIcon(nil),
715717
}
716718
row.content = container.NewBorder(
717719
nil,
@@ -789,7 +791,7 @@ func (r *queueRow) setRow(index int, item QueueItem, isCurrent bool) {
789791
}
790792
kind := item.MediaKind()
791793
go func() {
792-
apply(r.screen.queueMediaThumbnail(path, kind))
794+
apply(r.thumbnailLoader(path, kind))
793795
}()
794796
}
795797
}

internal/gui/queue_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import (
1010
"testing"
1111

1212
"github.com/alexballas/refyne/v2"
13+
"github.com/alexballas/refyne/v2/canvas"
1314
"github.com/alexballas/refyne/v2/test"
1415
"github.com/alexballas/refyne/v2/widget"
16+
"go2tv.app/go2tv/v2/internal/mediamodel"
1517
)
1618

1719
func newTraversalTestScreen(t *testing.T, currentPath string) *FyneScreen {
@@ -683,6 +685,12 @@ func TestQueueRowDedupesThumbnailRequests(t *testing.T) {
683685
row := newQueueRow(&FyneScreen{
684686
Debug: newDebugWriter(8),
685687
})
688+
releaseThumbnails := make(chan struct{})
689+
row.thumbnailLoader = func(string, mediamodel.MediaKind) *canvas.Image {
690+
<-releaseThumbnails
691+
return nil
692+
}
693+
defer close(releaseThumbnails)
686694

687695
first := testQueueItems(firstPath)[0]
688696
second := testQueueItems(secondPath)[0]

soapcalls/soapcallers.go

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1174,8 +1174,12 @@ func (p *TVPayload) GetProtocolInfo() error {
11741174

11751175
p.Log().Debug(string(resBytes), "Method", "GetProtocolInfo", "Action", "Response", "Status Code", strconv.Itoa(res.StatusCode), "Headers", json.RawMessage(headerBytesRes))
11761176

1177-
if err := parseProtocolInfo(resBytes, utils.DLNAResourceMediaType(p.MediaType, p.Transcode)); err != nil {
1178-
return fmt.Errorf("GetProtocolInfo Selected device does not support the media type: %w", err)
1177+
wireMediaType := utils.DLNAResourceMediaType(p.MediaType, p.Transcode)
1178+
if err := parseProtocolInfo(resBytes, wireMediaType); err != nil {
1179+
p.Log().Error("Media compatibility check failed", "Method", "GetProtocolInfo",
1180+
"MediaType", p.MediaType, "WireMediaType", wireMediaType,
1181+
"Transcode", p.Transcode, "MediaPath", p.MediaPath, "error", err)
1182+
return fmt.Errorf("GetProtocolInfo Selected device does not support the media type %q: %w", wireMediaType, err)
11791183
}
11801184

11811185
return nil
@@ -1573,8 +1577,6 @@ func parseProtocolInfo(b []byte, mt string) error {
15731577
return nil
15741578
}
15751579

1576-
mt = normalizeProtocolMediaType(mt)
1577-
15781580
if err := xml.Unmarshal(b, &respProtocolInfo); err != nil {
15791581
return err
15801582
}
@@ -1591,32 +1593,33 @@ func parseProtocolInfo(b []byte, mt string) error {
15911593
items := strings.SplitN(strings.TrimSpace(i), ":", 4)
15921594
// Here we hardcode check the http-get protocol. We would need to change that
15931595
// if we were to support rtp/rtsp/udp.
1594-
if len(items) == 4 && protocolInfoFieldMatches(items[0], "http-get") && protocolInfoFieldMatches(items[1], "*") && protocolInfoMediaTypeMatches(items[2], mt) {
1596+
if len(items) == 4 && protocolInfoFieldMatches(items[0], "http-get") && protocolInfoFieldMatches(items[1], "*") && protocolInfoMediaCategoryMatches(items[2], mt) {
15951597
return nil
15961598
}
15971599
}
15981600

15991601
return ErrNoMatchingFileType
16001602
}
16011603

1602-
func normalizeProtocolMediaType(mediaType string) string {
1603-
mediaType = strings.TrimSpace(mediaType)
1604-
if parameterStart := strings.IndexByte(mediaType, ';'); parameterStart >= 0 {
1605-
return strings.ToLower(mediaType[:parameterStart]) + mediaType[parameterStart:]
1606-
}
1607-
return strings.ToLower(mediaType)
1608-
}
1609-
16101604
func protocolInfoFieldMatches(advertised, requested string) bool {
16111605
advertised = strings.ToLower(strings.TrimSpace(advertised))
16121606
requested = strings.ToLower(strings.TrimSpace(requested))
16131607
return advertised == "*" || requested == "*" || advertised == requested
16141608
}
16151609

1616-
func protocolInfoMediaTypeMatches(advertised, requested string) bool {
1617-
advertised = normalizeProtocolMediaType(advertised)
1618-
requested = normalizeProtocolMediaType(requested)
1619-
return advertised == "*" || requested == "*" || advertised == requested
1610+
// protocolInfoMediaCategoryMatches is a permissive preflight check, as in 2.5.0.
1611+
// Renderers can omit playable formats or advertise alternate MIME names. Check
1612+
// only the category here; let playback determine container and codec support.
1613+
// This does not change the MIME used in HTTP headers or resource metadata.
1614+
func protocolInfoMediaCategoryMatches(advertised, requested string) bool {
1615+
advertised = strings.ToLower(strings.TrimSpace(advertised))
1616+
requested = strings.ToLower(strings.TrimSpace(requested))
1617+
if advertised == "*" || requested == "*" {
1618+
return true
1619+
}
1620+
advertisedCategory, _, hasSubtype := strings.Cut(advertised, "/")
1621+
requestedCategory, _, _ := strings.Cut(requested, "/")
1622+
return hasSubtype && advertisedCategory != "" && advertisedCategory == requestedCategory
16201623
}
16211624

16221625
func splitProtocolInfo(value string) []string {

soapcalls/soapcallers_test.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,28 +38,34 @@ func TestParseProtocolInfo(t *testing.T) {
3838
}
3939
}
4040

41-
func TestParseProtocolInfoMatchesContentFormat(t *testing.T) {
41+
func TestParseProtocolInfoMatchesMediaCategory(t *testing.T) {
4242
response := func(sink string) []byte {
4343
return []byte(`<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><u:GetProtocolInfoResponse xmlns:u="urn:schemas-upnp-org:service:ConnectionManager:1"><Sink>` + sink + `</Sink></u:GetProtocolInfoResponse></s:Body></s:Envelope>`)
4444
}
4545

46-
tests := []struct {
46+
tt := []struct {
4747
name string
4848
sink string
4949
mt string
5050
want bool
5151
}{
5252
{name: "exact MIME with different additional info", sink: `http-get:*:video/mp4:DLNA.ORG_CI=0`, mt: "video/mp4", want: true},
53-
{name: "matching MIME parameters", sink: `http-get:*:video/mp4;profile=main:*`, mt: "video/mp4;profile=main", want: true},
54-
{name: "different MIME parameters", sink: `http-get:*:video/mp4;profile=main:*`, mt: "video/mp4;profile=high", want: false},
55-
{name: "case-sensitive MIME parameter", sink: `http-get:*:multipart/mixed;boundary=CaseSensitive:*`, mt: "multipart/mixed;boundary=casesensitive", want: false},
56-
{name: "MIME subtype wildcard is not a wildcard", sink: `http-get:*:video/*:*`, mt: "video/x-matroska", want: false},
53+
{name: "Samsung Matroska alias", sink: `http-get:*:video/x-mkv:*`, mt: "video/x-matroska", want: true},
54+
{name: "unlisted video format", sink: `http-get:*:video/mpeg:*`, mt: "video/mp4", want: true},
55+
{name: "unlisted audio format", sink: `http-get:*:audio/mpeg:*`, mt: "audio/flac", want: true},
56+
{name: "unlisted image format", sink: `http-get:*:image/jpeg:*`, mt: "image/png", want: true},
57+
{name: "category case and whitespace", sink: `http-get:*: VIDEO/MPEG :*`, mt: " Video/MP4 ", want: true},
58+
{name: "MIME parameters do not restrict category", sink: `http-get:*:video/mp4;profile=main:*`, mt: "video/mp4;profile=high", want: true},
59+
{name: "video category wildcard", sink: `http-get:*:video/*:*`, mt: "video/x-matroska", want: true},
5760
{name: "whole MIME wildcard", sink: `http-get:*:*:*`, mt: "video/x-matroska", want: true},
5861
{name: "escaped feature comma", sink: `http-get:*:video/mp4:vendor=one\,two`, mt: "video/mp4", want: true},
59-
{name: "wrong MIME", sink: `http-get:*:audio/mpeg:*`, mt: "video/mp4", want: false},
62+
{name: "audio renderer rejects video", sink: `http-get:*:audio/mpeg:*`, mt: "video/mp4", want: false},
63+
{name: "audio wildcard rejects video", sink: `http-get:*:audio/*:*`, mt: "video/mp4", want: false},
64+
{name: "video renderer rejects images", sink: `http-get:*:video/mp4:*`, mt: "image/jpeg", want: false},
65+
{name: "wrong transport rejects same category", sink: `rtsp-rtp-udp:*:video/mpeg:*`, mt: "video/mp4", want: false},
6066
{name: "empty Sink is permissive", sink: ``, mt: "video/mp4", want: true},
6167
}
62-
for _, test := range tests {
68+
for _, test := range tt {
6369
t.Run(test.name, func(t *testing.T) {
6470
err := parseProtocolInfo(response(test.sink), test.mt)
6571
if (err == nil) != test.want {
@@ -340,15 +346,18 @@ func TestGetProtocolInfoSkipsWhenConnectionManagerMissing(t *testing.T) {
340346
}
341347
}
342348

343-
func TestGetProtocolInfoUsesTranscodedWireMIME(t *testing.T) {
349+
func TestGetProtocolInfoAllowsUnlistedVideoFormat(t *testing.T) {
344350
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
345351
_, _ = w.Write([]byte(`<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Body><u:GetProtocolInfoResponse xmlns:u="urn:schemas-upnp-org:service:ConnectionManager:1"><Sink>http-get:*:video/mpeg:*</Sink></u:GetProtocolInfoResponse></s:Body></s:Envelope>`))
346352
}))
347353
defer srv.Close()
348354

349-
p := &TVPayload{ConnectionManagerURL: srv.URL, MediaType: "video/x-matroska", Transcode: true}
355+
p := &TVPayload{ConnectionManagerURL: srv.URL, MediaType: "video/x-matroska"}
350356
if err := p.GetProtocolInfo(); err != nil {
351-
t.Fatalf("GetProtocolInfo() err = %v, want wire MIME match", err)
357+
t.Fatalf("GetProtocolInfo() err = %v, want video category match", err)
358+
}
359+
if p.MediaType != "video/x-matroska" {
360+
t.Fatalf("GetProtocolInfo() changed resource MIME to %q", p.MediaType)
352361
}
353362
}
354363

version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.6.0
1+
2.6.1

0 commit comments

Comments
 (0)