mirror of https://github.com/stashapp/stash.git
Add DASH streams for VP9 transcoding (#3275)
This commit is contained in:
parent
71e1451c94
commit
2d4384169a
1
go.mod
1
go.mod
|
@ -59,6 +59,7 @@ require (
|
|||
github.com/vektah/dataloaden v0.3.0
|
||||
github.com/vektah/gqlparser/v2 v2.4.2
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e
|
||||
github.com/zencoder/go-dash/v3 v3.0.2
|
||||
gopkg.in/guregu/null.v4 v4.0.0
|
||||
)
|
||||
|
||||
|
|
2
go.sum
2
go.sum
|
@ -781,6 +781,8 @@ github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
|
|||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/zencoder/go-dash/v3 v3.0.2 h1:oP1+dOh+Gp57PkvdCyMfbHtrHaxfl3w4kR3KBBbuqQE=
|
||||
github.com/zencoder/go-dash/v3 v3.0.2/go.mod h1:30R5bKy1aUYY45yesjtZ9l8trNc2TwNqbS17WVQmCzk=
|
||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
|
||||
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
|
|
|
@ -61,6 +61,9 @@ func (rs sceneRoutes) Routes() chi.Router {
|
|||
r.Get("/stream.mkv", rs.StreamMKV)
|
||||
r.Get("/stream.m3u8", rs.StreamHLS)
|
||||
r.Get("/stream.m3u8/{segment}.ts", rs.StreamHLSSegment)
|
||||
r.Get("/stream.mpd", rs.StreamDASH)
|
||||
r.Get("/stream.mpd/{segment}_v.webm", rs.StreamDASHVideoSegment)
|
||||
r.Get("/stream.mpd/{segment}_a.webm", rs.StreamDASHAudioSegment)
|
||||
|
||||
r.Get("/screenshot", rs.Screenshot)
|
||||
r.Get("/preview", rs.Preview)
|
||||
|
@ -168,6 +171,10 @@ func (rs sceneRoutes) StreamHLS(w http.ResponseWriter, r *http.Request) {
|
|||
rs.streamManifest(w, r, ffmpeg.StreamTypeHLS, "HLS")
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) StreamDASH(w http.ResponseWriter, r *http.Request) {
|
||||
rs.streamManifest(w, r, ffmpeg.StreamTypeDASHVideo, "DASH")
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) streamManifest(w http.ResponseWriter, r *http.Request, streamType *ffmpeg.StreamType, logName string) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
|
||||
|
@ -196,6 +203,14 @@ func (rs sceneRoutes) StreamHLSSegment(w http.ResponseWriter, r *http.Request) {
|
|||
rs.streamSegment(w, r, ffmpeg.StreamTypeHLS)
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) StreamDASHVideoSegment(w http.ResponseWriter, r *http.Request) {
|
||||
rs.streamSegment(w, r, ffmpeg.StreamTypeDASHVideo)
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) StreamDASHAudioSegment(w http.ResponseWriter, r *http.Request) {
|
||||
rs.streamSegment(w, r, ffmpeg.StreamTypeDASHAudio)
|
||||
}
|
||||
|
||||
func (rs sceneRoutes) streamSegment(w http.ResponseWriter, r *http.Request, streamType *ffmpeg.StreamType) {
|
||||
scene := r.Context().Value(sceneKey).(*models.Scene)
|
||||
|
||||
|
|
|
@ -50,6 +50,11 @@ var (
|
|||
mimeType: ffmpeg.MimeHLS,
|
||||
extension: ".m3u8",
|
||||
}
|
||||
dashEndpointType = endpointType{
|
||||
label: "DASH",
|
||||
mimeType: ffmpeg.MimeDASH,
|
||||
extension: ".mpd",
|
||||
}
|
||||
)
|
||||
|
||||
func GetVideoFileContainer(file *file.VideoFile) (ffmpeg.Container, error) {
|
||||
|
@ -163,46 +168,54 @@ func GetSceneStreamPaths(scene *models.Scene, directStreamURL *url.URL, maxStrea
|
|||
mp4Streams := []*SceneStreamEndpoint{}
|
||||
webmStreams := []*SceneStreamEndpoint{}
|
||||
hlsStreams := []*SceneStreamEndpoint{}
|
||||
dashStreams := []*SceneStreamEndpoint{}
|
||||
|
||||
if includeSceneStreamPath(models.StreamingResolutionEnumOriginal) {
|
||||
mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumOriginal))
|
||||
webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumOriginal))
|
||||
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumOriginal))
|
||||
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumOriginal))
|
||||
}
|
||||
|
||||
if includeSceneStreamPath(models.StreamingResolutionEnumFourK) {
|
||||
mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumFourK))
|
||||
webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumFourK))
|
||||
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumFourK))
|
||||
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumFourK))
|
||||
}
|
||||
|
||||
if includeSceneStreamPath(models.StreamingResolutionEnumFullHd) {
|
||||
mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumFullHd))
|
||||
webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumFullHd))
|
||||
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumFullHd))
|
||||
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumFullHd))
|
||||
}
|
||||
|
||||
if includeSceneStreamPath(models.StreamingResolutionEnumStandardHd) {
|
||||
mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumStandardHd))
|
||||
webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumStandardHd))
|
||||
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumStandardHd))
|
||||
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumStandardHd))
|
||||
}
|
||||
|
||||
if includeSceneStreamPath(models.StreamingResolutionEnumStandard) {
|
||||
mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumStandard))
|
||||
webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumStandard))
|
||||
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumStandard))
|
||||
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumStandard))
|
||||
}
|
||||
|
||||
if includeSceneStreamPath(models.StreamingResolutionEnumLow) {
|
||||
mp4Streams = append(mp4Streams, makeStreamEndpoint(mp4EndpointType, models.StreamingResolutionEnumLow))
|
||||
webmStreams = append(webmStreams, makeStreamEndpoint(webmEndpointType, models.StreamingResolutionEnumLow))
|
||||
hlsStreams = append(hlsStreams, makeStreamEndpoint(hlsEndpointType, models.StreamingResolutionEnumLow))
|
||||
dashStreams = append(dashStreams, makeStreamEndpoint(dashEndpointType, models.StreamingResolutionEnumLow))
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, mp4Streams...)
|
||||
endpoints = append(endpoints, webmStreams...)
|
||||
endpoints = append(endpoints, hlsStreams...)
|
||||
endpoints = append(endpoints, dashStreams...)
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
|
|
@ -20,11 +20,14 @@ import (
|
|||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
|
||||
"github.com/zencoder/go-dash/v3/mpd"
|
||||
)
|
||||
|
||||
const (
|
||||
MimeHLS string = "application/vnd.apple.mpegurl"
|
||||
MimeMpegTS string = "video/MP2T"
|
||||
MimeDASH string = "application/dash+xml"
|
||||
|
||||
segmentLength = 2
|
||||
|
||||
|
@ -81,7 +84,7 @@ var (
|
|||
"-avoid_negative_ts", "disabled",
|
||||
"-f", "hls",
|
||||
"-start_number", fmt.Sprint(segment),
|
||||
"-hls_time", "2",
|
||||
"-hls_time", fmt.Sprint(segmentLength),
|
||||
"-hls_segment_type", "mpegts",
|
||||
"-hls_playlist_type", "vod",
|
||||
"-hls_segment_filename", filepath.Join(outputDir, ".%d.ts"),
|
||||
|
@ -112,7 +115,7 @@ var (
|
|||
"-avoid_negative_ts", "disabled",
|
||||
"-f", "hls",
|
||||
"-start_number", fmt.Sprint(segment),
|
||||
"-hls_time", "2",
|
||||
"-hls_time", fmt.Sprint(segmentLength),
|
||||
"-hls_segment_type", "mpegts",
|
||||
"-hls_playlist_type", "vod",
|
||||
"-hls_segment_filename", filepath.Join(outputDir, ".%d.ts"),
|
||||
|
@ -121,6 +124,67 @@ var (
|
|||
return
|
||||
},
|
||||
}
|
||||
StreamTypeDASHVideo = &StreamType{
|
||||
Name: "dash-v",
|
||||
SegmentType: SegmentTypeWEBMVideo,
|
||||
ServeManifest: serveDASHManifest,
|
||||
Args: func(segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) {
|
||||
// only generate the actual init segment (init_v.webm)
|
||||
// when generating the first segment
|
||||
init := ".init"
|
||||
if segment == 0 {
|
||||
init = "init"
|
||||
}
|
||||
args = append(args,
|
||||
"-c:v", "libvpx-vp9",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-deadline", "realtime",
|
||||
"-cpu-used", "5",
|
||||
"-row-mt", "1",
|
||||
"-crf", "30",
|
||||
"-b:v", "0",
|
||||
"-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", segmentLength),
|
||||
)
|
||||
args = args.VideoFilter(videoFilter)
|
||||
args = append(args,
|
||||
"-copyts",
|
||||
"-avoid_negative_ts", "disabled",
|
||||
"-map", "0:v:0",
|
||||
"-f", "webm_chunk",
|
||||
"-chunk_start_index", fmt.Sprint(segment),
|
||||
"-header", filepath.Join(outputDir, init+"_v.webm"),
|
||||
filepath.Join(outputDir, ".%d_v.webm"),
|
||||
)
|
||||
return
|
||||
},
|
||||
}
|
||||
StreamTypeDASHAudio = &StreamType{
|
||||
Name: "dash-a",
|
||||
SegmentType: SegmentTypeWEBMAudio,
|
||||
ServeManifest: serveDASHManifest,
|
||||
Args: func(segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) {
|
||||
// only generate the actual init segment (init_a.webm)
|
||||
// when generating the first segment
|
||||
init := ".init"
|
||||
if segment == 0 {
|
||||
init = "init"
|
||||
}
|
||||
args = append(args,
|
||||
"-c:a", "libopus",
|
||||
"-b:a", "96000",
|
||||
"-ar", "48000",
|
||||
"-copyts",
|
||||
"-avoid_negative_ts", "disabled",
|
||||
"-map", "0:a:0",
|
||||
"-f", "webm_chunk",
|
||||
"-chunk_start_index", fmt.Sprint(segment),
|
||||
"-audio_chunk_duration", fmt.Sprint(segmentLength*1000),
|
||||
"-header", filepath.Join(outputDir, init+"_a.webm"),
|
||||
filepath.Join(outputDir, ".%d_a.webm"),
|
||||
)
|
||||
return
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type SegmentType struct {
|
||||
|
@ -145,6 +209,50 @@ var (
|
|||
return segment, err
|
||||
},
|
||||
}
|
||||
SegmentTypeWEBMVideo = &SegmentType{
|
||||
Format: "%d_v.webm",
|
||||
MimeType: MimeWebmVideo,
|
||||
MakeFilename: func(segment int) string {
|
||||
if segment == -1 {
|
||||
return "init_v.webm"
|
||||
} else {
|
||||
return fmt.Sprintf("%d_v.webm", segment)
|
||||
}
|
||||
},
|
||||
ParseSegment: func(str string) (int, error) {
|
||||
if str == "init" {
|
||||
return -1, nil
|
||||
} else {
|
||||
segment, err := strconv.Atoi(str)
|
||||
if err != nil || segment < 0 {
|
||||
err = ErrInvalidSegment
|
||||
}
|
||||
return segment, err
|
||||
}
|
||||
},
|
||||
}
|
||||
SegmentTypeWEBMAudio = &SegmentType{
|
||||
Format: "%d_a.webm",
|
||||
MimeType: MimeWebmAudio,
|
||||
MakeFilename: func(segment int) string {
|
||||
if segment == -1 {
|
||||
return "init_a.webm"
|
||||
} else {
|
||||
return fmt.Sprintf("%d_a.webm", segment)
|
||||
}
|
||||
},
|
||||
ParseSegment: func(str string) (int, error) {
|
||||
if str == "init" {
|
||||
return -1, nil
|
||||
} else {
|
||||
segment, err := strconv.Atoi(str)
|
||||
if err != nil || segment < 0 {
|
||||
err = ErrInvalidSegment
|
||||
}
|
||||
return segment, err
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
var ErrInvalidSegment = errors.New("invalid segment")
|
||||
|
@ -339,6 +447,97 @@ func serveHLSManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request,
|
|||
http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(buf.Bytes()))
|
||||
}
|
||||
|
||||
// serveDASHManifest serves a generated DASH manifest.
|
||||
func serveDASHManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request, vf *file.VideoFile, resolution string) {
|
||||
if sm.cacheDir == "" {
|
||||
logger.Error("[transcode] cannot live transcode with DASH because cache dir is unset")
|
||||
http.Error(w, "cannot live transcode files with DASH because cache dir is unset", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
probeResult, err := sm.ffprobe.NewVideoFile(vf.Path)
|
||||
if err != nil {
|
||||
logger.Warnf("[transcode] error generating DASH manifest: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var framerate string
|
||||
var videoWidth int
|
||||
var videoHeight int
|
||||
videoStream := probeResult.VideoStream
|
||||
if videoStream != nil {
|
||||
framerate = videoStream.AvgFrameRate
|
||||
videoWidth = videoStream.Width
|
||||
videoHeight = videoStream.Height
|
||||
} else {
|
||||
// extract the framerate fraction from the file framerate
|
||||
// framerates 0.1% below round numbers are common,
|
||||
// attempt to infer when this is the case
|
||||
fileFramerate := vf.FrameRate
|
||||
rate1001, off1001 := math.Modf(fileFramerate * 1.001)
|
||||
var numerator int
|
||||
var denominator int
|
||||
switch {
|
||||
case off1001 < 0.005:
|
||||
numerator = int(rate1001) * 1000
|
||||
denominator = 1001
|
||||
case off1001 > 0.995:
|
||||
numerator = (int(rate1001) + 1) * 1000
|
||||
denominator = 1001
|
||||
default:
|
||||
numerator = int(fileFramerate * 1000)
|
||||
denominator = 1000
|
||||
}
|
||||
framerate = fmt.Sprintf("%d/%d", numerator, denominator)
|
||||
videoHeight = vf.Height
|
||||
videoWidth = vf.Width
|
||||
}
|
||||
|
||||
var urlQuery string
|
||||
maxTranscodeSize := sm.config.GetMaxStreamingTranscodeSize().GetMaxResolution()
|
||||
if resolution != "" {
|
||||
maxTranscodeSize = models.StreamingResolutionEnum(resolution).GetMaxResolution()
|
||||
urlQuery = fmt.Sprintf("?resolution=%s", resolution)
|
||||
}
|
||||
if maxTranscodeSize != 0 {
|
||||
videoSize := videoHeight
|
||||
if videoWidth < videoSize {
|
||||
videoSize = videoWidth
|
||||
}
|
||||
|
||||
if maxTranscodeSize < videoSize {
|
||||
scaleFactor := float64(maxTranscodeSize) / float64(videoSize)
|
||||
videoWidth = int(float64(videoWidth) * scaleFactor)
|
||||
videoHeight = int(float64(videoHeight) * scaleFactor)
|
||||
}
|
||||
}
|
||||
|
||||
mediaDuration := mpd.Duration(time.Duration(probeResult.FileDuration * float64(time.Second)))
|
||||
m := mpd.NewMPD(mpd.DASH_PROFILE_LIVE, mediaDuration.String(), "PT4.0S")
|
||||
|
||||
baseUrl := r.URL.JoinPath("/")
|
||||
baseUrl.RawQuery = ""
|
||||
m.BaseURL = baseUrl.String()
|
||||
|
||||
video, _ := m.AddNewAdaptationSetVideo(MimeWebmVideo, "progressive", true, 1)
|
||||
|
||||
_, _ = video.SetNewSegmentTemplate(2, "init_v.webm"+urlQuery, "$Number$_v.webm"+urlQuery, 0, 1)
|
||||
_, _ = video.AddNewRepresentationVideo(200000, "vp09.00.40.08", "0", framerate, int64(videoWidth), int64(videoHeight))
|
||||
|
||||
if ProbeAudioCodec(vf.AudioCodec) != MissingUnsupported {
|
||||
audio, _ := m.AddNewAdaptationSetAudio(MimeWebmAudio, true, 1, "und")
|
||||
_, _ = audio.SetNewSegmentTemplate(2, "init_a.webm"+urlQuery, "$Number$_a.webm"+urlQuery, 0, 1)
|
||||
_, _ = audio.AddNewRepresentationAudio(48000, 96000, "opus", "1")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = m.Write(&buf)
|
||||
|
||||
w.Header().Set("Content-Type", MimeDASH)
|
||||
http.ServeContent(w, r, "", time.Time{}, bytes.NewReader(buf.Bytes()))
|
||||
}
|
||||
|
||||
func (sm *StreamManager) ServeManifest(w http.ResponseWriter, r *http.Request, streamType *StreamType, vf *file.VideoFile, resolution string) {
|
||||
streamType.ServeManifest(sm, w, r, vf, resolution)
|
||||
}
|
||||
|
@ -355,7 +554,6 @@ func (sm *StreamManager) serveWaitingSegment(w http.ResponseWriter, r *http.Requ
|
|||
w.Header().Add("Cache-Control", "no-cache")
|
||||
http.ServeFile(w, r, segment.path)
|
||||
} else if !errors.Is(err, context.Canceled) {
|
||||
logger.Errorf("[transcode] %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
@ -442,6 +640,7 @@ func (sm *StreamManager) startTranscode(stream *runningStream, segment int, done
|
|||
logger.Debugf("[transcode] starting transcode for %s at segment #%d", stream.dir, segment)
|
||||
|
||||
if err := os.MkdirAll(stream.outputDir, os.ModePerm); err != nil {
|
||||
logger.Errorf("[transcode] %v", err)
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
|
@ -464,7 +663,9 @@ func (sm *StreamManager) startTranscode(stream *runningStream, segment int, done
|
|||
logger.Tracef("[transcode] running %s", cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
lockCtx.Cancel()
|
||||
done <- fmt.Errorf("error starting transcode process: %w", err)
|
||||
err = fmt.Errorf("error starting transcode process: %w", err)
|
||||
logger.Errorf("[transcode] %v", err)
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -499,7 +700,12 @@ func (sm *StreamManager) startTranscode(stream *runningStream, segment int, done
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
err = fmt.Errorf("[transcode] ffmpeg error when running command <%s>: %w", strings.Join(cmd.Args, " "), err)
|
||||
err = fmt.Errorf("ffmpeg error when running command <%s>: %w", strings.Join(cmd.Args, " "), err)
|
||||
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
logger.Errorf("[transcode] %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -517,7 +723,9 @@ func (sm *StreamManager) startTranscode(stream *runningStream, segment int, done
|
|||
|
||||
sm.streamsMutex.Unlock()
|
||||
|
||||
done <- err
|
||||
if err != nil {
|
||||
done <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
|
@ -562,7 +770,9 @@ func (s *waitingSegment) checkAvailable(now time.Time) bool {
|
|||
s.available <- nil
|
||||
return true
|
||||
} else if s.accessed.Add(maxSegmentWait).Before(now) {
|
||||
s.available <- fmt.Errorf("timed out waiting for segment file %s to be generated", s.file)
|
||||
err := fmt.Errorf("timed out waiting for segment file %s to be generated", s.file)
|
||||
logger.Errorf("[transcode] %v", err)
|
||||
s.available <- err
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
|
@ -63,7 +63,8 @@
|
|||
"string.prototype.replaceall": "^1.0.7",
|
||||
"thehandy": "^1.0.3",
|
||||
"universal-cookie": "^4.0.4",
|
||||
"video.js": "^7.21.2",
|
||||
"video.js": "^7.21.3",
|
||||
"videojs-contrib-dash": "^5.1.1",
|
||||
"videojs-mobile-ui": "^0.8.0",
|
||||
"videojs-seek-buttons": "^3.0.1",
|
||||
"videojs-vtt.js": "^0.15.4",
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
declare module "videojs-contrib-dash" {
|
||||
class Html5DashJS {
|
||||
/**
|
||||
* Get a list of hooks for a specific lifecycle.
|
||||
*
|
||||
* @param type the lifecycle to get hooks from
|
||||
* @param hook optionally add a hook to the lifecycle
|
||||
* @return an array of hooks or empty if none
|
||||
*/
|
||||
static hooks(type: string, hook: Function | Function[]): Function[];
|
||||
|
||||
/**
|
||||
* Add a function hook to a specific dash lifecycle.
|
||||
*
|
||||
* @param type the lifecycle to hook the function to
|
||||
* @param hook the function or array of functions to attach
|
||||
*/
|
||||
static hook(type: string, hook: Function | Function[]): void;
|
||||
|
||||
/**
|
||||
* Remove a hook from a specific dash lifecycle.
|
||||
*
|
||||
* @param type the lifecycle that the function hooked to
|
||||
* @param hook the hooked function to remove
|
||||
* @return true if the function was removed, false if not found
|
||||
*/
|
||||
static removeHook(type: string, hook: Function): boolean;
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import React, {
|
|||
useState,
|
||||
} from "react";
|
||||
import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js";
|
||||
import "videojs-contrib-dash";
|
||||
import "videojs-mobile-ui";
|
||||
import "videojs-seek-buttons";
|
||||
import "./live";
|
||||
|
@ -267,6 +268,25 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
|||
},
|
||||
chaptersButton: false,
|
||||
},
|
||||
html5: {
|
||||
nativeTextTracks: false,
|
||||
dash: {
|
||||
updateSettings: [
|
||||
{
|
||||
streaming: {
|
||||
buffer: {
|
||||
bufferTimeAtTopQuality: 30,
|
||||
bufferTimeAtTopQualityLongForm: 30,
|
||||
},
|
||||
gaps: {
|
||||
jumpGaps: false,
|
||||
jumpLargeGaps: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
nativeControlsForTouch: false,
|
||||
playbackRates: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
|
||||
inactivityTimeout: 2000,
|
||||
|
@ -463,6 +483,7 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
|||
const src = new URL(stream.url);
|
||||
const isDirect =
|
||||
src.pathname.endsWith("/stream") ||
|
||||
src.pathname.endsWith("/stream.mpd") ||
|
||||
src.pathname.endsWith("/stream.m3u8");
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
##### 💥 Note: The cache directory is now required if using HLS streaming. Please set the cache directory in the System Settings page.
|
||||
|
||||
### ✨ New Features
|
||||
* Added support for DASH streaming. ([#3275](https://github.com/stashapp/stash/pull/3275))
|
||||
* Added configuration option for the maximum number of items in selector drop-downs. ([#3277](https://github.com/stashapp/stash/pull/3277))
|
||||
* Added configuration option to perform generation operations sequentially after scanning a new video file. ([#3378](https://github.com/stashapp/stash/pull/3378))
|
||||
* Optionally show range in generated funscript heatmaps. ([#3373](https://github.com/stashapp/stash/pull/3373))
|
||||
|
|
|
@ -2540,10 +2540,10 @@
|
|||
"@typescript-eslint/types" "5.52.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@videojs/http-streaming@2.16.0":
|
||||
version "2.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-2.16.0.tgz#47f86e793fc773db26dbd9aeb3e5130b767c78fa"
|
||||
integrity sha512-mGNTqjENzP86XGM6HSWdWVO/KAsDlf5+idW2W7dL1+NkzWpwZlSEYhrdEVVnhoOb0A6E7JW6LM611/JA7Jn/3A==
|
||||
"@videojs/http-streaming@2.16.2":
|
||||
version "2.16.2"
|
||||
resolved "https://registry.yarnpkg.com/@videojs/http-streaming/-/http-streaming-2.16.2.tgz#a9be925b4e368a41dbd67d49c4f566715169b84b"
|
||||
integrity sha512-etPTUdCFu7gUWc+1XcbiPr+lrhOcBu3rV5OL1M+3PDW89zskScAkkcdqYzP4pFodBPye/ydamQoTDScOnElw5A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@videojs/vhs-utils" "3.0.5"
|
||||
|
@ -3049,6 +3049,28 @@ base64-js@^1.3.1:
|
|||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
bcp-47-match@^1.0.0, bcp-47-match@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/bcp-47-match/-/bcp-47-match-1.0.3.tgz#cb8d03071389a10aff2062b862d6575ffd7cd7ef"
|
||||
integrity sha512-LggQ4YTdjWQSKELZF5JwchnBa1u0pIQSZf5lSdOHEdbVP55h0qICA/FUp3+W99q0xqxYa1ZQizTUH87gecII5w==
|
||||
|
||||
bcp-47-normalize@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/bcp-47-normalize/-/bcp-47-normalize-1.1.1.tgz#d2c76218d132f223c44e4a06a7224be3030f8ec3"
|
||||
integrity sha512-jWZ1Jdu3cs0EZdfCkS0UE9Gg01PtxnChjEBySeB+Zo6nkqtFfnvtoQQgP1qU1Oo4qgJgxhTI6Sf9y/pZIhPs0A==
|
||||
dependencies:
|
||||
bcp-47 "^1.0.0"
|
||||
bcp-47-match "^1.0.0"
|
||||
|
||||
bcp-47@^1.0.0:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/bcp-47/-/bcp-47-1.0.8.tgz#bf63ae4269faabe7c100deac0811121a48b6a561"
|
||||
integrity sha512-Y9y1QNBBtYtv7hcmoX0tR+tUNSFZGZ6OL6vKPObq8BbOhkCoyayF6ogfLTgAli/KuAEbsYHYUNq2AQuY6IuLag==
|
||||
dependencies:
|
||||
is-alphabetical "^1.0.0"
|
||||
is-alphanumerical "^1.0.0"
|
||||
is-decimal "^1.0.0"
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
|
@ -3339,6 +3361,11 @@ clone@^1.0.2:
|
|||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
|
||||
|
||||
codem-isoboxer@0.3.6:
|
||||
version "0.3.6"
|
||||
resolved "https://registry.yarnpkg.com/codem-isoboxer/-/codem-isoboxer-0.3.6.tgz#867f670459b881d44f39168d5ff2a8f14c16151d"
|
||||
integrity sha512-LuO8/7LW6XuR5ERn1yavXAfodGRhuY2yP60JTZIw5yNYMCE5lUVbk3NFUCJxjnphQH+Xemp5hOGb1LgUXm00Xw==
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
|
@ -3511,6 +3538,22 @@ damerau-levenshtein@^1.0.8:
|
|||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
|
||||
|
||||
dashjs@^4.2.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/dashjs/-/dashjs-4.6.0.tgz#124c8371e192f1218746ce60b6aa0f175d4dcda4"
|
||||
integrity sha512-0PDoSBM9PXb+Io0pRnw2CmO7aV9W8FC/BqBRNhLxzM3/e5Kfj7BLy0OWkkSB58ULg6Md6r+6jkGOTUhut/35rg==
|
||||
dependencies:
|
||||
bcp-47-match "^1.0.3"
|
||||
bcp-47-normalize "^1.1.1"
|
||||
codem-isoboxer "0.3.6"
|
||||
es6-promise "^4.2.8"
|
||||
fast-deep-equal "2.0.1"
|
||||
html-entities "^1.2.1"
|
||||
imsc "^1.0.2"
|
||||
localforage "^1.7.1"
|
||||
path-browserify "^1.0.1"
|
||||
ua-parser-js "^1.0.2"
|
||||
|
||||
dataloader@2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.2.tgz#216dc509b5abe39d43a9b9d97e6e5e473dfbe3e0"
|
||||
|
@ -3786,6 +3829,11 @@ es-to-primitive@^1.2.1:
|
|||
is-date-object "^1.0.1"
|
||||
is-symbol "^1.0.2"
|
||||
|
||||
es6-promise@^4.2.8:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
||||
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
|
||||
|
||||
esbuild@^0.16.14:
|
||||
version "0.16.17"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259"
|
||||
|
@ -4116,6 +4164,11 @@ fast-decode-uri-component@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543"
|
||||
integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==
|
||||
|
||||
fast-deep-equal@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
|
||||
integrity sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==
|
||||
|
||||
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
|
@ -4424,7 +4477,7 @@ global-prefix@^3.0.0:
|
|||
kind-of "^6.0.2"
|
||||
which "^1.3.1"
|
||||
|
||||
global@^4.3.1, global@^4.4.0, global@~4.4.0:
|
||||
global@^4.3.1, global@^4.3.2, global@^4.4.0, global@~4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406"
|
||||
integrity sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==
|
||||
|
@ -4636,6 +4689,11 @@ hosted-git-info@^4.0.1:
|
|||
dependencies:
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
html-entities@^1.2.1:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.4.0.tgz#cfbd1b01d2afaf9adca1b10ae7dffab98c71d2dc"
|
||||
integrity sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==
|
||||
|
||||
html-tags@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961"
|
||||
|
@ -4715,6 +4773,13 @@ import-lazy@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153"
|
||||
integrity sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==
|
||||
|
||||
imsc@^1.0.2:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/imsc/-/imsc-1.1.3.tgz#e96a60a50d4000dd7b44097272768b9fd6a4891d"
|
||||
integrity sha512-IY0hMkVTNoqoYwKEp5UvNNKp/A5jeJUOrIO7judgOyhHT+xC6PA4VBOMAOhdtAYbMRHx9DTgI8p6Z6jhYQPFDA==
|
||||
dependencies:
|
||||
sax "1.2.1"
|
||||
|
||||
imurmurhash@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
|
||||
|
@ -5333,7 +5398,7 @@ load-json-file@^6.2.0:
|
|||
strip-bom "^4.0.0"
|
||||
type-fest "^0.6.0"
|
||||
|
||||
localforage@^1.10.0:
|
||||
localforage@^1.10.0, localforage@^1.7.1:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4"
|
||||
integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==
|
||||
|
@ -6138,6 +6203,11 @@ pascal-case@^3.1.2:
|
|||
no-case "^3.0.4"
|
||||
tslib "^2.0.3"
|
||||
|
||||
path-browserify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
|
||||
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
|
||||
|
||||
path-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/path-case/-/path-case-3.0.4.tgz#9168645334eb942658375c56f80b4c0cb5f82c6f"
|
||||
|
@ -6623,13 +6693,6 @@ regexpp@^3.2.0:
|
|||
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
|
||||
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
|
||||
|
||||
rehype-react@^6.0.0:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-6.2.1.tgz#9b9bf188451ad6f63796b784fe1f51165c67b73a"
|
||||
integrity sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg==
|
||||
dependencies:
|
||||
"@mapbox/hast-util-table-cell-style" "^0.2.0"
|
||||
hast-to-hyperscript "^9.0.0"
|
||||
regexpu-core@^5.3.1:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.1.tgz#66900860f88def39a5cb79ebd9490e84f17bcdfb"
|
||||
|
@ -6649,6 +6712,14 @@ regjsparser@^0.9.1:
|
|||
dependencies:
|
||||
jsesc "~0.5.0"
|
||||
|
||||
rehype-react@^6.0.0:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/rehype-react/-/rehype-react-6.2.1.tgz#9b9bf188451ad6f63796b784fe1f51165c67b73a"
|
||||
integrity sha512-f9KIrjktvLvmbGc7si25HepocOg4z0MuNOtweigKzBcDjiGSTGhyz6VSgaV5K421Cq1O+z4/oxRJ5G9owo0KVg==
|
||||
dependencies:
|
||||
"@mapbox/hast-util-table-cell-style" "^0.2.0"
|
||||
hast-to-hyperscript "^9.0.0"
|
||||
|
||||
relay-runtime@12.0.0:
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/relay-runtime/-/relay-runtime-12.0.0.tgz#1e039282bdb5e0c1b9a7dc7f6b9a09d4f4ff8237"
|
||||
|
@ -6851,6 +6922,11 @@ sass@^1.58.1:
|
|||
immutable "^4.0.0"
|
||||
source-map-js ">=0.6.2 <2.0.0"
|
||||
|
||||
sax@1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
|
||||
integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==
|
||||
|
||||
scheduler@^0.20.2:
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
|
||||
|
@ -7021,10 +7097,10 @@ source-map@^0.6.0:
|
|||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
space-separated-tokens@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f"
|
||||
integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
|
||||
space-separated-tokens@^1.0.0:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
|
||||
integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==
|
||||
|
||||
spdx-correct@^3.0.0:
|
||||
version "3.1.1"
|
||||
|
@ -7535,6 +7611,11 @@ ua-parser-js@^0.7.30:
|
|||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
|
||||
integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==
|
||||
|
||||
ua-parser-js@^1.0.2:
|
||||
version "1.0.33"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.33.tgz#f21f01233e90e7ed0f059ceab46eb190ff17f8f4"
|
||||
integrity sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==
|
||||
|
||||
unbox-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
|
||||
|
@ -7583,10 +7664,10 @@ unicode-property-aliases-ecmascript@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd"
|
||||
integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==
|
||||
|
||||
unified@^10.0.0:
|
||||
version "10.1.2"
|
||||
resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df"
|
||||
integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==
|
||||
unified@^9.0.0:
|
||||
version "9.2.2"
|
||||
resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.2.tgz#67649a1abfc3ab85d2969502902775eb03146975"
|
||||
integrity sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==
|
||||
dependencies:
|
||||
bail "^1.0.0"
|
||||
extend "^3.0.0"
|
||||
|
@ -7775,13 +7856,13 @@ vfile@^4.0.0:
|
|||
unist-util-stringify-position "^2.0.0"
|
||||
vfile-message "^2.0.0"
|
||||
|
||||
"video.js@^6 || ^7", video.js@^7.21.2:
|
||||
version "7.21.2"
|
||||
resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.21.2.tgz#2dbf17b435690be739b15748bd53d3002f548e3a"
|
||||
integrity sha512-Zbo23oT4CbtIxeAtfTvzdl7OlN/P34ir7hDzXFtLZB+BtJsaLy0Rgh/06dBMJSGEjQCDo4MUS6uPonuX0Nl3Kg==
|
||||
"video.js@^5.18.0 || ^6 || ^7", "video.js@^6 || ^7", video.js@^7.21.3:
|
||||
version "7.21.3"
|
||||
resolved "https://registry.yarnpkg.com/video.js/-/video.js-7.21.3.tgz#1a5f6379e713de3f5dc036ecdef02efb80765bdd"
|
||||
integrity sha512-fIboXbSDCT3P8eVzIEC3hnLDKC/y+6QftcHdFGUVGn5a7qmH62Mh0Bt/SrBAgdmKDQM1qdZXfXAxPg5+IaiIXQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@videojs/http-streaming" "2.16.0"
|
||||
"@videojs/http-streaming" "2.16.2"
|
||||
"@videojs/vhs-utils" "^3.0.4"
|
||||
"@videojs/xhr" "2.6.0"
|
||||
aes-decrypter "3.1.3"
|
||||
|
@ -7794,6 +7875,15 @@ vfile@^4.0.0:
|
|||
videojs-font "3.2.0"
|
||||
videojs-vtt.js "^0.15.4"
|
||||
|
||||
videojs-contrib-dash@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/videojs-contrib-dash/-/videojs-contrib-dash-5.1.1.tgz#9f50191677815a7d816c500977811a926aee0643"
|
||||
integrity sha512-MI0kPHuQ3KH9Mc2mLVLqvFKCoEyTfXzHc02fm8pqMk8v7LXrJKnIv9xfugBccRF7vZHDZISftedD/CmEJfvvrA==
|
||||
dependencies:
|
||||
dashjs "^4.2.0"
|
||||
global "^4.3.2"
|
||||
video.js "^5.18.0 || ^6 || ^7"
|
||||
|
||||
videojs-font@3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-3.2.0.tgz#212c9d3f4e4ec3fa7345167d64316add35e92232"
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
Copyright Brightcove, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,49 @@
|
|||
package ptrs
|
||||
|
||||
func Strptr(v string) *string {
|
||||
p := new(string)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
func Intptr(v int) *int {
|
||||
p := new(int)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
func Int64ptr(v int64) *int64 {
|
||||
p := new(int64)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
func Uintptr(v uint) *uint {
|
||||
p := new(uint)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
func Uint32ptr(v uint32) *uint32 {
|
||||
p := new(uint32)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
func Uint64ptr(v uint64) *uint64 {
|
||||
p := new(uint64)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
func Boolptr(v bool) *bool {
|
||||
p := new(bool)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
func Float64ptr(v float64) *float64 {
|
||||
p := new(float64)
|
||||
*p = v
|
||||
return p
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
// based on code from golang src/time/time.go
|
||||
|
||||
package mpd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Duration time.Duration
|
||||
|
||||
var (
|
||||
rStart = "^P" // Must start with a 'P'
|
||||
rDays = "(\\d+D)?" // We only allow Days for durations, not Months or Years
|
||||
rTime = "(?:T" // If there's any 'time' units then they must be preceded by a 'T'
|
||||
rHours = "(\\d+H)?" // Hours
|
||||
rMinutes = "(\\d+M)?" // Minutes
|
||||
rSeconds = "([\\d.]+S)?" // Seconds (Potentially decimal)
|
||||
rEnd = ")?$" // end of regex must close "T" capture group
|
||||
)
|
||||
|
||||
var xmlDurationRegex = regexp.MustCompile(rStart + rDays + rTime + rHours + rMinutes + rSeconds + rEnd)
|
||||
|
||||
func (d Duration) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
|
||||
return xml.Attr{Name: name, Value: d.String()}, nil
|
||||
}
|
||||
|
||||
func (d *Duration) UnmarshalXMLAttr(attr xml.Attr) error {
|
||||
dur, err := ParseDuration(attr.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*d = Duration(dur)
|
||||
return nil
|
||||
}
|
||||
|
||||
// String renders a Duration in XML Duration Data Type format
|
||||
func (d *Duration) String() string {
|
||||
// Largest time is 2540400h10m10.000000000s
|
||||
var buf [32]byte
|
||||
w := len(buf)
|
||||
|
||||
u := uint64(*d)
|
||||
neg := *d < 0
|
||||
if neg {
|
||||
u = -u
|
||||
}
|
||||
|
||||
if u < uint64(time.Second) {
|
||||
// Special case: if duration is smaller than a second,
|
||||
// use smaller units, like 1.2ms
|
||||
var prec int
|
||||
w--
|
||||
buf[w] = 'S'
|
||||
w--
|
||||
if u == 0 {
|
||||
return "PT0S"
|
||||
}
|
||||
/*
|
||||
switch {
|
||||
case u < uint64(Millisecond):
|
||||
// print microseconds
|
||||
prec = 3
|
||||
// U+00B5 'µ' micro sign == 0xC2 0xB5
|
||||
w-- // Need room for two bytes.
|
||||
copy(buf[w:], "µ")
|
||||
default:
|
||||
// print milliseconds
|
||||
prec = 6
|
||||
buf[w] = 'm'
|
||||
}
|
||||
*/
|
||||
w, u = fmtFrac(buf[:w], u, prec)
|
||||
w = fmtInt(buf[:w], u)
|
||||
} else {
|
||||
w--
|
||||
buf[w] = 'S'
|
||||
|
||||
w, u = fmtFrac(buf[:w], u, 9)
|
||||
|
||||
// u is now integer seconds
|
||||
w = fmtInt(buf[:w], u%60)
|
||||
u /= 60
|
||||
|
||||
// u is now integer minutes
|
||||
if u > 0 {
|
||||
w--
|
||||
buf[w] = 'M'
|
||||
w = fmtInt(buf[:w], u%60)
|
||||
u /= 60
|
||||
|
||||
// u is now integer hours
|
||||
// Stop at hours because days can be different lengths.
|
||||
if u > 0 {
|
||||
w--
|
||||
buf[w] = 'H'
|
||||
w = fmtInt(buf[:w], u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if neg {
|
||||
w--
|
||||
buf[w] = '-'
|
||||
}
|
||||
|
||||
return "PT" + string(buf[w:])
|
||||
}
|
||||
|
||||
// fmtFrac formats the fraction of v/10**prec (e.g., ".12345") into the
|
||||
// tail of buf, omitting trailing zeros. it omits the decimal
|
||||
// point too when the fraction is 0. It returns the index where the
|
||||
// output bytes begin and the value v/10**prec.
|
||||
func fmtFrac(buf []byte, v uint64, prec int) (nw int, nv uint64) {
|
||||
// Omit trailing zeros up to and including decimal point.
|
||||
w := len(buf)
|
||||
print := false
|
||||
for i := 0; i < prec; i++ {
|
||||
digit := v % 10
|
||||
print = print || digit != 0
|
||||
if print {
|
||||
w--
|
||||
buf[w] = byte(digit) + '0'
|
||||
}
|
||||
v /= 10
|
||||
}
|
||||
if print {
|
||||
w--
|
||||
buf[w] = '.'
|
||||
}
|
||||
return w, v
|
||||
}
|
||||
|
||||
// fmtInt formats v into the tail of buf.
|
||||
// It returns the index where the output begins.
|
||||
func fmtInt(buf []byte, v uint64) int {
|
||||
w := len(buf)
|
||||
if v == 0 {
|
||||
w--
|
||||
buf[w] = '0'
|
||||
} else {
|
||||
for v > 0 {
|
||||
w--
|
||||
buf[w] = byte(v%10) + '0'
|
||||
v /= 10
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func ParseDuration(str string) (time.Duration, error) {
|
||||
if len(str) < 3 {
|
||||
return 0, errors.New("At least one number and designator are required")
|
||||
}
|
||||
|
||||
if strings.Contains(str, "-") {
|
||||
return 0, errors.New("Duration cannot be negative")
|
||||
}
|
||||
|
||||
// Check that only the parts we expect exist and that everything's in the correct order
|
||||
if !xmlDurationRegex.Match([]byte(str)) {
|
||||
return 0, errors.New("Duration must be in the format: P[nD][T[nH][nM][nS]]")
|
||||
}
|
||||
|
||||
var parts = xmlDurationRegex.FindStringSubmatch(str)
|
||||
var total time.Duration
|
||||
|
||||
if parts[1] != "" {
|
||||
days, err := strconv.Atoi(strings.TrimRight(parts[1], "D"))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Error parsing Days: %s", err)
|
||||
}
|
||||
total += time.Duration(days) * time.Hour * 24
|
||||
}
|
||||
|
||||
if parts[2] != "" {
|
||||
hours, err := strconv.Atoi(strings.TrimRight(parts[2], "H"))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Error parsing Hours: %s", err)
|
||||
}
|
||||
total += time.Duration(hours) * time.Hour
|
||||
}
|
||||
|
||||
if parts[3] != "" {
|
||||
mins, err := strconv.Atoi(strings.TrimRight(parts[3], "M"))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Error parsing Minutes: %s", err)
|
||||
}
|
||||
total += time.Duration(mins) * time.Minute
|
||||
}
|
||||
|
||||
if parts[4] != "" {
|
||||
secs, err := strconv.ParseFloat(strings.TrimRight(parts[4], "S"), 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("Error parsing Seconds: %s", err)
|
||||
}
|
||||
total += time.Duration(secs * float64(time.Second))
|
||||
}
|
||||
|
||||
return total, nil
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package mpd
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
type EventStream struct {
|
||||
XMLName xml.Name `xml:"EventStream"`
|
||||
SchemeIDURI *string `xml:"schemeIdUri,attr"`
|
||||
Value *string `xml:"value,attr,omitempty"`
|
||||
Timescale *uint `xml:"timescale,attr"`
|
||||
Events []Event `xml:"Event,omitempty"`
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
XMLName xml.Name `xml:"Event"`
|
||||
ID *string `xml:"id,attr,omitempty"`
|
||||
PresentationTime *uint64 `xml:"presentationTime,attr,omitempty"`
|
||||
Duration *uint64 `xml:"duration,attr,omitempty"`
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,57 @@
|
|||
package mpd
|
||||
|
||||
type AttrMPD interface {
|
||||
GetStrptr() *string
|
||||
}
|
||||
|
||||
type attrAvailabilityStartTime struct {
|
||||
strptr *string
|
||||
}
|
||||
|
||||
func (attr *attrAvailabilityStartTime) GetStrptr() *string {
|
||||
return attr.strptr
|
||||
}
|
||||
|
||||
// AttrAvailabilityStartTime returns AttrMPD object for NewMPD
|
||||
func AttrAvailabilityStartTime(value string) AttrMPD {
|
||||
return &attrAvailabilityStartTime{strptr: &value}
|
||||
}
|
||||
|
||||
type attrMinimumUpdatePeriod struct {
|
||||
strptr *string
|
||||
}
|
||||
|
||||
func (attr *attrMinimumUpdatePeriod) GetStrptr() *string {
|
||||
return attr.strptr
|
||||
}
|
||||
|
||||
// AttrMinimumUpdatePeriod returns AttrMPD object for NewMPD
|
||||
func AttrMinimumUpdatePeriod(value string) AttrMPD {
|
||||
return &attrMinimumUpdatePeriod{strptr: &value}
|
||||
}
|
||||
|
||||
type attrMediaPresentationDuration struct {
|
||||
strptr *string
|
||||
}
|
||||
|
||||
func (attr *attrMediaPresentationDuration) GetStrptr() *string {
|
||||
return attr.strptr
|
||||
}
|
||||
|
||||
// AttrMediaPresentationDuration returns AttrMPD object for NewMPD
|
||||
func AttrMediaPresentationDuration(value string) AttrMPD {
|
||||
return &attrMediaPresentationDuration{strptr: &value}
|
||||
}
|
||||
|
||||
type attrPublishTime struct {
|
||||
strptr *string
|
||||
}
|
||||
|
||||
func (attr *attrPublishTime) GetStrptr() *string {
|
||||
return attr.strptr
|
||||
}
|
||||
|
||||
// AttrPublishTime returns AttrMPD object for NewMPD
|
||||
func AttrPublishTime(value string) AttrMPD {
|
||||
return &attrPublishTime{strptr: &value}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package mpd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Reads an MPD XML file from disk into a MPD object.
|
||||
// path - File path to an MPD on disk
|
||||
func ReadFromFile(path string) (*MPD, error) {
|
||||
f, err := os.OpenFile(path, os.O_RDONLY, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return Read(f)
|
||||
}
|
||||
|
||||
// Reads a string into a MPD object.
|
||||
// xmlStr - MPD manifest data as a string.
|
||||
func ReadFromString(xmlStr string) (*MPD, error) {
|
||||
b := bytes.NewBufferString(xmlStr)
|
||||
return Read(b)
|
||||
}
|
||||
|
||||
// Reads from an io.Reader interface into an MPD object.
|
||||
// r - Must implement the io.Reader interface.
|
||||
func Read(r io.Reader) (*MPD, error) {
|
||||
var mpd MPD
|
||||
d := xml.NewDecoder(r)
|
||||
err := d.Decode(&mpd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mpd, nil
|
||||
}
|
||||
|
||||
// Writes an MPD object to a file on disk.
|
||||
// path - Output path to write the manifest to.
|
||||
func (m *MPD) WriteToFile(path string) error {
|
||||
// Open the file to write the XML to
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if err = m.Write(f); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = f.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Writes an MPD object to a string.
|
||||
func (m *MPD) WriteToString() (string, error) {
|
||||
var b bytes.Buffer
|
||||
w := bufio.NewWriter(&b)
|
||||
err := m.Write(w)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = w.Flush()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return b.String(), err
|
||||
}
|
||||
|
||||
// Writes an MPD object to an io.Writer interface
|
||||
// w - Must implement the io.Writer interface.
|
||||
func (m *MPD) Write(w io.Writer) error {
|
||||
b, err := xml.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte(xml.Header))
|
||||
_, _ = w.Write(b)
|
||||
_, _ = w.Write([]byte("\n"))
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package mpd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func MakePSSHBox(systemID, payload []byte) ([]byte, error) {
|
||||
if len(systemID) != 16 {
|
||||
return nil, fmt.Errorf("SystemID must be 16 bytes, was: %d", len(systemID))
|
||||
}
|
||||
|
||||
psshBuf := &bytes.Buffer{}
|
||||
size := uint32(12 + 16 + 4 + len(payload)) // 3 uint32s, systemID, "pssh" string and payload
|
||||
if err := binary.Write(psshBuf, binary.BigEndian, size); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Write(psshBuf, binary.BigEndian, []byte("pssh")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Write(psshBuf, binary.BigEndian, uint32(0)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := psshBuf.Write(systemID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := binary.Write(psshBuf, binary.BigEndian, uint32(len(payload))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := psshBuf.Write(payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return psshBuf.Bytes(), nil
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package mpd
|
||||
|
||||
type SegmentBase struct {
|
||||
Initialization *URL `xml:"Initialization,omitempty"`
|
||||
RepresentationIndex *URL `xml:"RepresentationIndex,omitempty"`
|
||||
Timescale *uint32 `xml:"timescale,attr,omitempty"`
|
||||
PresentationTimeOffset *uint64 `xml:"presentationTimeOffset,attr,omitempty"`
|
||||
IndexRange *string `xml:"indexRange,attr,omitempty"`
|
||||
IndexRangeExact *bool `xml:"indexRangeExact,attr,omitempty"`
|
||||
AvailabilityTimeOffset *float32 `xml:"availabilityTimeOffset,attr,omitempty"`
|
||||
AvailabilityTimeComplete *bool `xml:"availabilityTimeComplete,attr,omitempty"`
|
||||
}
|
||||
|
||||
type MultipleSegmentBase struct {
|
||||
SegmentBase
|
||||
SegmentTimeline *SegmentTimeline `xml:"SegmentTimeline,omitempty"`
|
||||
BitstreamSwitching *URL `xml:"BitstreamSwitching,omitempty"`
|
||||
Duration *uint32 `xml:"duration,attr,omitempty"`
|
||||
StartNumber *uint32 `xml:"startNumber,attr,omitempty"`
|
||||
}
|
||||
|
||||
type SegmentList struct {
|
||||
MultipleSegmentBase
|
||||
SegmentURLs []*SegmentURL `xml:"SegmentURL,omitempty"`
|
||||
}
|
||||
|
||||
type SegmentURL struct {
|
||||
Media *string `xml:"media,attr,omitempty"`
|
||||
MediaRange *string `xml:"mediaRange,attr,omitempty"`
|
||||
Index *string `xml:"index,attr,omitempty"`
|
||||
IndexRange *string `xml:"indexRange,attr,omitempty"`
|
||||
}
|
||||
|
||||
type SegmentTimeline struct {
|
||||
Segments []*SegmentTimelineSegment `xml:"S,omitempty"`
|
||||
}
|
||||
|
||||
type SegmentTimelineSegment struct {
|
||||
StartTime *uint64 `xml:"t,attr,omitempty"`
|
||||
Duration uint64 `xml:"d,attr"`
|
||||
RepeatCount *int `xml:"r,attr,omitempty"`
|
||||
}
|
||||
|
||||
type URL struct {
|
||||
SourceURL *string `xml:"sourceURL,attr,omitempty"`
|
||||
Range *string `xml:"range,attr,omitempty"`
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package mpd
|
||||
|
||||
// Validate checks for incomplete MPD object
|
||||
func (m *MPD) Validate() error {
|
||||
if m.Profiles == nil {
|
||||
return ErrNoDASHProfileSet
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -404,6 +404,10 @@ github.com/xWTF/chardet
|
|||
# github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673
|
||||
## explicit
|
||||
github.com/xrash/smetrics
|
||||
# github.com/zencoder/go-dash/v3 v3.0.2
|
||||
## explicit; go 1.13
|
||||
github.com/zencoder/go-dash/v3/helpers/ptrs
|
||||
github.com/zencoder/go-dash/v3/mpd
|
||||
# go.uber.org/atomic v1.7.0
|
||||
## explicit; go 1.13
|
||||
go.uber.org/atomic
|
||||
|
|
Loading…
Reference in New Issue