+
Skip to content

new feature: live streams generated on the fly via .dms.json #112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -57,6 +60,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 @@ -89,3 +94,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
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载