dlna: support for external srt subtitles

Allows for filename.srt, filename.en.srt, etc., to be automatically associated with video.mp4 (or whatever) when playing over dlna.

This is the "modern" method, which I've verified to work on VLC and in LG webOS 2.  There is a vendor specific mechanism for Samsung that I havn't been able to get working on my F series.

Also made some minor corrections to logging and container IDs.
This commit is contained in:
Dan Walters 2019-09-22 18:58:24 -05:00 committed by Nick Craig-Wood
parent d210fecf3b
commit 6337cc70d3
2 changed files with 78 additions and 12 deletions

View File

@ -46,6 +46,7 @@ func init() {
{"video/webm", ".webm"}, {"video/webm", ".webm"},
{"video/x-msvideo", ".avi"}, {"video/x-msvideo", ".avi"},
{"video/x-matroska", ".mpv,.mkv"}, {"video/x-matroska", ".mpv,.mkv"},
{"text/srt", ".srt"},
} { } {
for _, ext := range strings.Split(t.extensions, ",") { for _, ext := range strings.Split(t.extensions, ",") {
err := mime.AddExtensionType(ext, t.mimeType) err := mime.AddExtensionType(ext, t.mimeType)
@ -69,7 +70,7 @@ var mediaMimeTypeRegexp = regexp.MustCompile("^(video|audio|image)/")
// Turns the given entry and DMS host into a UPnP object. A nil object is // Turns the given entry and DMS host into a UPnP object. A nil object is
// returned if the entry is not of interest. // returned if the entry is not of interest.
func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fileInfo vfs.Node, host string) (ret interface{}, err error) { func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fileInfo vfs.Node, resources vfs.Nodes, host string) (ret interface{}, err error) {
obj := upnpav.Object{ obj := upnpav.Object{
ID: cdsObject.ID(), ID: cdsObject.ID(),
Restricted: 1, Restricted: 1,
@ -128,12 +129,25 @@ func (cds *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fi
ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{ ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", mimeType, dlna.ContentFeatures{
SupportRange: true, SupportRange: true,
}.String()), }.String()),
Bitrate: 0,
Duration: "",
Size: uint64(fileInfo.Size()), Size: uint64(fileInfo.Size()),
Resolution: "",
}) })
basePath, _ := path.Split(cdsObject.Path)
for _, resource := range resources {
subtitleURL := (&url.URL{
Scheme: "http",
Host: host,
Path: resPath,
RawQuery: url.Values{
"path": {basePath + resource.Path()},
}.Encode(),
}).String()
item.Res = append(item.Res, upnpav.Resource{
URL: subtitleURL,
ProtocolInfo: fmt.Sprintf("http-get:*:%s:*", "text/srt"),
})
}
ret = item ret = item
return return
} }
@ -157,13 +171,13 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret []
return return
} }
sort.Sort(dirEntries) dirEntries, extraResources := partitionExtraResources(dirEntries)
for _, de := range dirEntries { for _, de := range dirEntries {
child := object{ child := object{
path.Join(o.Path, de.Name()), path.Join(o.Path, de.Name()),
} }
obj, err := cds.cdsObjectToUpnpavObject(child, de, host) obj, err := cds.cdsObjectToUpnpavObject(child, de, extraResources[de], host)
if err != nil { if err != nil {
fs.Errorf(cds, "error with %s: %s", child.FilePath(), err) fs.Errorf(cds, "error with %s: %s", child.FilePath(), err)
continue continue
@ -178,6 +192,53 @@ func (cds *contentDirectoryService) readContainer(o object, host string) (ret []
return return
} }
// Given a list of nodes, separate them into potential media items and any associated resources (external subtitles,
// thumbnails, metadata, etc.)
func partitionExtraResources(nodes vfs.Nodes) (vfs.Nodes, map[vfs.Node]vfs.Nodes) {
// First, separate out the subtitles into a separate list from the media
media, subtitles := make(vfs.Nodes, 0), make(vfs.Nodes, 0)
for _, node := range nodes {
name := strings.ToLower(node.Name()) // case insensitive
switch path.Ext(name) {
case ".srt":
subtitles = append(subtitles, node)
default:
media = append(media, node)
}
}
// Find the associated media file for each subtitle
extraResources := make(map[vfs.Node]vfs.Nodes)
for _, node := range subtitles {
subtitleName := strings.ToLower(node.Name())
// For a media file named "My Video.mp4", we want to associated any subtitles named like
// "My Video.srt", "My Video.en.srt", "My Video.es.srt", "My Video.forced.srt"
// note: nodes must be sorted! vfs.dir.ReadDirAll() results are already sorted ..
mediaIdx := sort.Search(len(media), func(idx int) bool {
mediaName := strings.ToLower(media[idx].Name())
basename := strings.SplitN(mediaName, ".", 2)[0]
if strings.Compare(subtitleName, basename) <= 0 {
return true
}
if strings.HasPrefix(subtitleName, basename) {
return subtitleName[len(basename)] == '.'
}
return false
})
if mediaIdx == -1 {
fs.Infof(node, "could not find associated media for subtitle: %s", node.Name())
continue
}
mediaNode := media[mediaIdx]
fs.Debugf(mediaNode, "associating subtitle: %s", node.Name())
extraResources[mediaNode] = append(extraResources[mediaNode], node)
}
return media, extraResources
}
type browse struct { type browse struct {
ObjectID string ObjectID string
BrowseFlag string BrowseFlag string
@ -256,7 +317,8 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt
if err != nil { if err != nil {
return nil, err return nil, err
} }
upnpObject, err := cds.cdsObjectToUpnpavObject(obj, node, host) // TODO: External subtitles won't appear in the metadata here, but probably should.
upnpObject, err := cds.cdsObjectToUpnpavObject(obj, node, vfs.Nodes{}, host)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -279,9 +341,9 @@ func (cds *contentDirectoryService) Handle(action string, argsXML []byte, r *htt
return map[string]string{ return map[string]string{
"FeatureList": `<Features xmlns="urn:schemas-upnp-org:av:avs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd"> "FeatureList": `<Features xmlns="urn:schemas-upnp-org:av:avs" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd">
<Feature name="samsung.com_BASICVIEW" version="1"> <Feature name="samsung.com_BASICVIEW" version="1">
<container id="/" type="object.item.imageItem"/> <container id="0" type="object.item.imageItem"/>
<container id="/" type="object.item.audioItem"/> <container id="0" type="object.item.audioItem"/>
<container id="/" type="object.item.videoItem"/> <container id="0" type="object.item.videoItem"/>
</Feature> </Feature>
</Features>`}, nil </Features>`}, nil
case "X_SetBookmark": case "X_SetBookmark":

View File

@ -134,7 +134,11 @@ func (lrw *loggingResponseWriter) logRequest(code int, err interface{}) {
level = fs.LogLevelError level = fs.LogLevelError
} }
fs.LogPrintf(level, lrw.request.URL.Path, "%s %s %d %s %s", if err == nil {
err = ""
}
fs.LogPrintf(level, lrw.request.URL, "%s %s %d %s %s",
lrw.request.RemoteAddr, lrw.request.Method, code, lrw.request.RemoteAddr, lrw.request.Method, code,
lrw.request.Header.Get("SOAPACTION"), err) lrw.request.Header.Get("SOAPACTION"), err)
} }