Skip to content

Commit

Permalink
new feature: live streams generated on the fly via .dms.json config f…
Browse files Browse the repository at this point in the history
…iles (#112)

bugfix: Samsung Frame TVs crashed when fetching transcoded/live streams due to HEAD request
  • Loading branch information
irsl authored Mar 13, 2023
1 parent c6ea09b commit 5bbc905
Show file tree
Hide file tree
Showing 6 changed files with 353 additions and 35 deletions.
26 changes: 26 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ dms advertises and serves the raw files, in addition to alternate transcoded
streams when it's able, such as mpeg2 PAL-DVD and WebM for the Chromecast. It
will also provide thumbnails where possible.

dms also supports serving dynamic streams (e.g. a live rtsp stream) generated
on the fly with the help of an external application (e.g. ffmpeg).

dms uses ``ffprobe``/``avprobe`` to get media data such as bitrate and duration, ``ffmpeg``/``avconv`` for video transoding, and ``ffmpegthumbnailer`` for generating thumbnails when browsing. These commands must be in the ``PATH`` given to ``dms`` or the features requiring them will be disabled.

.. image:: https://i.imgur.com/qbHilI7.png
Expand Down Expand Up @@ -67,6 +70,8 @@ Usage of dms:

* - parameter
- description
* - ``-allowDynamicStreams``
- turns on support for `.dms.json` files in the path
* - ``-allowedIps string``
- allowed ip of clients, separated by comma
* - ``-config string``
Expand Down Expand Up @@ -99,3 +104,24 @@ Usage of dms:
- browse root path
* - ``-stallEventSubscribe``
- workaround for some bad event subscribers

Dynamic streams
===============
DMS supports "dynamic streams" generated on the fly. This feature can be activated with the
`-allowDynamicStreams` command line flag and can be configured by placing special metadata
files in your content directory.
The name of these metadata files ends with `.dms.json`, their structure is [documented here](https://pkg.go.dev/github.com/anacrolix/dms/dlna/dms)

An example:

```
{
"Title": "My awesome webcam",
"Resources": [
{
"MimeType": "video/webm",
"Command": "ffmpeg -i rtsp://10.6.8.161:554/Streaming/Channels/502/ -c:v copy -c:a copy -movflags +faststart+frag_keyframe+empty_moov -f matroska -"
}
]
}
```
10 changes: 9 additions & 1 deletion dlna/dlna.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type ContentFeatures struct {
SupportRange bool
// Play speeds, DLNA.ORG_PS would go here if supported.
Transcoded bool
// DLNA.ORG_FLAGS go here if you need to tweak.
Flags string
}

func BinaryInt(b bool) uint {
Expand All @@ -41,7 +43,13 @@ func (cf ContentFeatures) String() (ret string) {
BinaryInt(cf.SupportTimeSeek),
BinaryInt(cf.SupportRange),
BinaryInt(cf.Transcoded)))
params = append(params, "DLNA.ORG_FLAGS=01700000000000000000000000000000")
// https://stackoverflow.com/questions/29182754/c-dlna-generate-dlna-org-flags
// DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE | DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE | DLNA_ORG_FLAG_CONNECTION_STALL | DLNA_ORG_FLAG_DLNA_V15
flags := "01700000000000000000000000000000"
if cf.Flags != "" {
flags = cf.Flags
}
params = append(params, "DLNA.ORG_FLAGS="+flags)
return strings.Join(params, ";")
}

Expand Down
137 changes: 136 additions & 1 deletion dlna/dms/cds.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package dms

import (
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"

"github.com/anacrolix/log"
Expand All @@ -20,6 +23,8 @@ import (
"github.com/anacrolix/ffprobe"
)

const dmsMetadataSuffix = ".dms.json"

type contentDirectoryService struct {
*Server
upnp.Eventing
Expand All @@ -29,6 +34,127 @@ func (cds *contentDirectoryService) updateIDString() string {
return fmt.Sprintf("%d", uint32(os.Getpid()))
}

type dmsDynamicStreamResource struct {
// (optional) DLNA profile name to include in the response e.g. MPEG_PS_PAL
DlnaProfileName string
// (optional) DLNA.ORG_FLAGS if you need to override the default (8D500000000000000000000000000000)
DlnaFlags string
// required: mime type, e.g. video/mpeg
MimeType string
// (optional) resolution, e.g. 640x360
Resolution string
// (optional) bitrate, e.g. 721
Bitrate uint
// required: OS command to generate this resource on the fly
Command string
}

type dmsDynamicMediaItem struct {
// (optional) Title of this media item. Defaults to the filename, if omitted
Title string
// (optional) duration, e.g. 0:21:37.922
Duration string
// required: an array of available versions
Resources []dmsDynamicStreamResource
}

func readDynamicStream(metadataPath string) (*dmsDynamicMediaItem, error) {
bytes, err := ioutil.ReadFile(metadataPath)
if err != nil {
return nil, err
}
var re dmsDynamicMediaItem
err = json.Unmarshal(bytes, &re)
if err != nil {
return nil, err
}
return &re, nil
}

func (me *contentDirectoryService) cdsObjectDynamicStreamToUpnpavObject(cdsObject object, fileInfo os.FileInfo, host, userAgent string) (ret interface{}, err error) {
// at this point we know that entryFilePath points to a .dms.json file; slurp and parse
dmsMediaItem, err := readDynamicStream(cdsObject.FilePath())
if err != nil {
me.Logger.Printf("%s ignored: %v", cdsObject.FilePath(), err)
return
}

obj := upnpav.Object{
ID: cdsObject.ID(),
Restricted: 1,
ParentID: cdsObject.ParentID(),
}
iconURI := (&url.URL{
Scheme: "http",
Host: host,
Path: iconPath,
RawQuery: url.Values{
"path": {cdsObject.Path},
}.Encode(),
}).String()
obj.Icon = iconURI
// TODO(anacrolix): This might not be necessary due to item res image
// element.
obj.AlbumArtURI = iconURI
obj.Class = "object.item.videoItem"

obj.Title = dmsMediaItem.Title
if obj.Title == "" {
obj.Title = strings.TrimSuffix(fileInfo.Name(), dmsMetadataSuffix)
}

item := upnpav.Item{
Object: obj,
// Capacity: 1 for icon, plus resources.
Res: make([]upnpav.Resource, 0, 1+len(dmsMediaItem.Resources)),
}
for i, dmsStream := range dmsMediaItem.Resources {
// default flags borrowed from Serviio: DLNA_ORG_FLAG_SENDER_PACED | DLNA_ORG_FLAG_S0_INCREASE | DLNA_ORG_FLAG_SN_INCREASE | DLNA_ORG_FLAG_STREAMING_TRANSFER_MODE | DLNA_ORG_FLAG_BACKGROUND_TRANSFERT_MODE | DLNA_ORG_FLAG_DLNA_V15
flags := "8D500000000000000000000000000000"
if dmsStream.DlnaFlags != "" {
flags = dmsStream.DlnaFlags
}
item.Res = append(item.Res, upnpav.Resource{
URL: (&url.URL{
Scheme: "http",
Host: host,
Path: resPath,
RawQuery: url.Values{
"path": {cdsObject.Path},
"index": {strconv.Itoa(i)},
}.Encode(),
}).String(),
ProtocolInfo: fmt.Sprintf("http-get:*:%s:%s", dmsStream.MimeType, dlna.ContentFeatures{
ProfileName: dmsStream.DlnaProfileName,
SupportRange: false,
SupportTimeSeek: false,
Transcoded: true,
Flags: flags,
}.String()),
Bitrate: dmsStream.Bitrate,
Duration: dmsMediaItem.Duration,
Resolution: dmsStream.Resolution,
})
}

// and an icon
item.Res = append(item.Res, upnpav.Resource{
URL: (&url.URL{
Scheme: "http",
Host: host,
Path: iconPath,
RawQuery: url.Values{
"path": {cdsObject.Path},
"c": {"jpeg"},
}.Encode(),
}).String(),
ProtocolInfo: "http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN",
})

ret = item
return
}

// Turns the given entry and DMS host into a UPnP object. A nil object is
// returned if the entry is not of interest.
func (me *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fileInfo os.FileInfo, host, userAgent string) (ret interface{}, err error) {
Expand All @@ -40,6 +166,11 @@ func (me *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fil
if ignored {
return
}
isDmsMetadata := strings.HasSuffix(entryFilePath, dmsMetadataSuffix)
if !fileInfo.IsDir() && me.AllowDynamicStreams && isDmsMetadata {
return me.cdsObjectDynamicStreamToUpnpavObject(cdsObject, fileInfo, host, userAgent)
}

obj := upnpav.Object{
ID: cdsObject.ID(),
Restricted: 1,
Expand All @@ -60,7 +191,11 @@ func (me *contentDirectoryService) cdsObjectToUpnpavObject(cdsObject object, fil
return
}
if !mimeType.IsMedia() {
me.Logger.Printf("%s ignored: non-media file (%s)", cdsObject.FilePath(), mimeType)
if isDmsMetadata {
me.Logger.Printf("%s ignored: enable support for dynamic streams via the -allowDynamicStreams command line flag", cdsObject.FilePath())
} else {
me.Logger.Printf("%s ignored: non-media file (%s)", cdsObject.FilePath(), mimeType)
}
return
}
iconURI := (&url.URL{
Expand Down
Loading

0 comments on commit 5bbc905

Please sign in to comment.