From 2d4384169a817fa5a0fdb07d3120ad84489c6263 Mon Sep 17 00:00:00 2001 From: DingDongSoLong4 <99329275+DingDongSoLong4@users.noreply.github.com> Date: Tue, 7 Mar 2023 03:57:27 +0200 Subject: [PATCH] Add DASH streams for VP9 transcoding (#3275) --- go.mod | 1 + go.sum | 2 + internal/api/routes_scene.go | 15 + internal/manager/scene.go | 13 + pkg/ffmpeg/stream_segmented.go | 224 +++- ui/v2.5/package.json | 3 +- ui/v2.5/src/@types/videojs-contrib-dash.d.ts | 31 + .../components/ScenePlayer/ScenePlayer.tsx | 21 + ui/v2.5/src/docs/en/Changelog/v0200.md | 1 + ui/v2.5/yarn.lock | 142 +- vendor/github.com/zencoder/go-dash/v3/LICENSE | 13 + .../zencoder/go-dash/v3/helpers/ptrs/ptrs.go | 49 + .../zencoder/go-dash/v3/mpd/duration.go | 206 +++ .../zencoder/go-dash/v3/mpd/events.go | 18 + .../github.com/zencoder/go-dash/v3/mpd/mpd.go | 1168 +++++++++++++++++ .../zencoder/go-dash/v3/mpd/mpd_attr.go | 57 + .../zencoder/go-dash/v3/mpd/mpd_read_write.go | 87 ++ .../zencoder/go-dash/v3/mpd/pssh.go | 41 + .../zencoder/go-dash/v3/mpd/segment.go | 47 + .../zencoder/go-dash/v3/mpd/validate.go | 9 + vendor/modules.txt | 4 + 21 files changed, 2118 insertions(+), 34 deletions(-) create mode 100644 ui/v2.5/src/@types/videojs-contrib-dash.d.ts create mode 100644 vendor/github.com/zencoder/go-dash/v3/LICENSE create mode 100644 vendor/github.com/zencoder/go-dash/v3/helpers/ptrs/ptrs.go create mode 100644 vendor/github.com/zencoder/go-dash/v3/mpd/duration.go create mode 100644 vendor/github.com/zencoder/go-dash/v3/mpd/events.go create mode 100644 vendor/github.com/zencoder/go-dash/v3/mpd/mpd.go create mode 100644 vendor/github.com/zencoder/go-dash/v3/mpd/mpd_attr.go create mode 100644 vendor/github.com/zencoder/go-dash/v3/mpd/mpd_read_write.go create mode 100644 vendor/github.com/zencoder/go-dash/v3/mpd/pssh.go create mode 100644 vendor/github.com/zencoder/go-dash/v3/mpd/segment.go create mode 100644 vendor/github.com/zencoder/go-dash/v3/mpd/validate.go diff --git a/go.mod b/go.mod index fdee11b68..1fbf6858a 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index d5926da5a..14bf5606a 100644 --- a/go.sum +++ b/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= diff --git a/internal/api/routes_scene.go b/internal/api/routes_scene.go index e4597b6a3..f4162b396 100644 --- a/internal/api/routes_scene.go +++ b/internal/api/routes_scene.go @@ -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) diff --git a/internal/manager/scene.go b/internal/manager/scene.go index 6abb192b4..a653cb632 100644 --- a/internal/manager/scene.go +++ b/internal/manager/scene.go @@ -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 } diff --git a/pkg/ffmpeg/stream_segmented.go b/pkg/ffmpeg/stream_segmented.go index c1184d14c..3632bf779 100644 --- a/pkg/ffmpeg/stream_segmented.go +++ b/pkg/ffmpeg/stream_segmented.go @@ -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 diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 6038530a4..f52844a90 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -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", diff --git a/ui/v2.5/src/@types/videojs-contrib-dash.d.ts b/ui/v2.5/src/@types/videojs-contrib-dash.d.ts new file mode 100644 index 000000000..b791d1edb --- /dev/null +++ b/ui/v2.5/src/@types/videojs-contrib-dash.d.ts @@ -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; + } +} diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 032b41efc..5ecc43a90 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -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 = ({ }, 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 = ({ const src = new URL(stream.url); const isDirect = src.pathname.endsWith("/stream") || + src.pathname.endsWith("/stream.mpd") || src.pathname.endsWith("/stream.m3u8"); return { diff --git a/ui/v2.5/src/docs/en/Changelog/v0200.md b/ui/v2.5/src/docs/en/Changelog/v0200.md index b73117145..6ea39e30a 100644 --- a/ui/v2.5/src/docs/en/Changelog/v0200.md +++ b/ui/v2.5/src/docs/en/Changelog/v0200.md @@ -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)) diff --git a/ui/v2.5/yarn.lock b/ui/v2.5/yarn.lock index 4cc7d952c..16fae3619 100644 --- a/ui/v2.5/yarn.lock +++ b/ui/v2.5/yarn.lock @@ -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" diff --git a/vendor/github.com/zencoder/go-dash/v3/LICENSE b/vendor/github.com/zencoder/go-dash/v3/LICENSE new file mode 100644 index 000000000..d9569559f --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/LICENSE @@ -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. diff --git a/vendor/github.com/zencoder/go-dash/v3/helpers/ptrs/ptrs.go b/vendor/github.com/zencoder/go-dash/v3/helpers/ptrs/ptrs.go new file mode 100644 index 000000000..9072660cf --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/helpers/ptrs/ptrs.go @@ -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 +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/duration.go b/vendor/github.com/zencoder/go-dash/v3/mpd/duration.go new file mode 100644 index 000000000..8ebc87021 --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/duration.go @@ -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 +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/events.go b/vendor/github.com/zencoder/go-dash/v3/mpd/events.go new file mode 100644 index 000000000..096291876 --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/events.go @@ -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"` +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/mpd.go b/vendor/github.com/zencoder/go-dash/v3/mpd/mpd.go new file mode 100644 index 000000000..57e718b4a --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/mpd.go @@ -0,0 +1,1168 @@ +package mpd + +import ( + "encoding/base64" + "encoding/hex" + "encoding/xml" + "errors" + "strings" + "time" + + . "github.com/zencoder/go-dash/v3/helpers/ptrs" +) + +// Type definition for DASH profiles +type DashProfile string + +// Constants for supported DASH profiles +const ( + // Live Profile + DASH_PROFILE_LIVE DashProfile = "urn:mpeg:dash:profile:isoff-live:2011" + // On Demand Profile + DASH_PROFILE_ONDEMAND DashProfile = "urn:mpeg:dash:profile:isoff-on-demand:2011" + // HbbTV Profile + DASH_PROFILE_HBBTV_1_5_LIVE DashProfile = "urn:hbbtv:dash:profile:isoff-live:2012,urn:mpeg:dash:profile:isoff-live:2011" +) + +type AudioChannelConfigurationScheme string + +const ( + // Scheme for non-Dolby Audio + AUDIO_CHANNEL_CONFIGURATION_MPEG_DASH AudioChannelConfigurationScheme = "urn:mpeg:dash:23003:3:audio_channel_configuration:2011" + // Scheme for Dolby Audio + AUDIO_CHANNEL_CONFIGURATION_MPEG_DOLBY AudioChannelConfigurationScheme = "tag:dolby.com,2014:dash:audio_channel_configuration:2011" +) + +// AccessibilityElementScheme is the scheme definition for an Accessibility element +type AccessibilityElementScheme string + +// Accessibility descriptor values for Audio Description +const ACCESSIBILITY_ELEMENT_SCHEME_DESCRIPTIVE_AUDIO AccessibilityElementScheme = "urn:tva:metadata:cs:AudioPurposeCS:2007" + +// Constants for some known MIME types, this is a limited list and others can be used. +const ( + DASH_MIME_TYPE_VIDEO_MP4 string = "video/mp4" + DASH_MIME_TYPE_AUDIO_MP4 string = "audio/mp4" + DASH_MIME_TYPE_SUBTITLE_VTT string = "text/vtt" + DASH_MIME_TYPE_SUBTITLE_TTML string = "application/ttaf+xml" + DASH_MIME_TYPE_SUBTITLE_SRT string = "application/x-subrip" + DASH_MIME_TYPE_SUBTITLE_DFXP string = "application/ttaf+xml" + DASH_MIME_TYPE_IMAGE_JPEG string = "image/jpeg" + DASH_CONTENT_TYPE_IMAGE string = "image" +) + +// Known error variables +var ( + ErrNoDASHProfileSet error = errors.New("No DASH profile set") + ErrAdaptationSetNil = errors.New("Adaptation Set nil") + ErrSegmentTemplateLiveProfileOnly = errors.New("Segment template can only be used with Live Profile") + ErrSegmentTemplateNil = errors.New("Segment Template nil ") + ErrRepresentationNil = errors.New("Representation nil") + ErrAccessibilityNil = errors.New("Accessibility nil") + ErrBaseURLEmpty = errors.New("Base URL empty") + ErrSegmentBaseOnDemandProfileOnly = errors.New("Segment Base can only be used with On-Demand Profile") + ErrSegmentBaseNil = errors.New("Segment Base nil") + ErrAudioChannelConfigurationNil = errors.New("Audio Channel Configuration nil") + ErrInvalidDefaultKID = errors.New("Invalid Default KID string, should be 32 characters") + ErrPROEmpty = errors.New("PlayReady PRO empty") + ErrContentProtectionNil = errors.New("Content Protection nil") +) + +type MPD struct { + XMLNs *string `xml:"xmlns,attr"` + Profiles *string `xml:"profiles,attr"` + Type *string `xml:"type,attr"` + MediaPresentationDuration *string `xml:"mediaPresentationDuration,attr"` + MinBufferTime *string `xml:"minBufferTime,attr"` + AvailabilityStartTime *string `xml:"availabilityStartTime,attr,omitempty"` + MinimumUpdatePeriod *string `xml:"minimumUpdatePeriod,attr"` + PublishTime *string `xml:"publishTime,attr"` + TimeShiftBufferDepth *string `xml:"timeShiftBufferDepth,attr"` + SuggestedPresentationDelay *Duration `xml:"suggestedPresentationDelay,attr,omitempty"` + BaseURL string `xml:"BaseURL,omitempty"` + Location string `xml:"Location,omitempty"` + period *Period + Periods []*Period `xml:"Period,omitempty"` + UTCTiming *DescriptorType `xml:"UTCTiming,omitempty"` +} + +type Period struct { + ID string `xml:"id,attr,omitempty"` + Duration Duration `xml:"duration,attr,omitempty"` + Start *Duration `xml:"start,attr,omitempty"` + BaseURL string `xml:"BaseURL,omitempty"` + SegmentBase *SegmentBase `xml:"SegmentBase,omitempty"` + SegmentList *SegmentList `xml:"SegmentList,omitempty"` + SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate,omitempty"` + AdaptationSets []*AdaptationSet `xml:"AdaptationSet,omitempty"` + EventStreams []EventStream `xml:"EventStream,omitempty"` +} + +type DescriptorType struct { + SchemeIDURI *string `xml:"schemeIdUri,attr"` + Value *string `xml:"value,attr"` + ID *string `xml:"id,attr"` +} + +// ISO 23009-1-2014 5.3.7 +type CommonAttributesAndElements struct { + Profiles *string `xml:"profiles,attr"` + Width *string `xml:"width,attr"` + Height *string `xml:"height,attr"` + Sar *string `xml:"sar,attr"` + FrameRate *string `xml:"frameRate,attr"` + AudioSamplingRate *string `xml:"audioSamplingRate,attr"` + MimeType *string `xml:"mimeType,attr"` + SegmentProfiles *string `xml:"segmentProfiles,attr"` + Codecs *string `xml:"codecs,attr"` + MaximumSAPPeriod *string `xml:"maximumSAPPeriod,attr"` + StartWithSAP *int64 `xml:"startWithSAP,attr"` + MaxPlayoutRate *string `xml:"maxPlayoutRate,attr"` + ScanType *string `xml:"scanType,attr"` + FramePacking []DescriptorType `xml:"FramePacking,omitempty"` + AudioChannelConfiguration []DescriptorType `xml:"AudioChannelConfiguration,omitempty"` + ContentProtection []ContentProtectioner `xml:"ContentProtection,omitempty"` + EssentialProperty []DescriptorType `xml:"EssentialProperty,omitempty"` + SupplementalProperty []DescriptorType `xml:"SupplementalProperty,omitempty"` + InbandEventStream *DescriptorType `xml:"inbandEventStream,attr"` +} + +type contentProtections []ContentProtectioner + +func (as *contentProtections) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var scheme string + for _, a := range start.Attr { + if a.Name.Local == "schemeIdUri" { + scheme = a.Value + break + } + } + var target ContentProtectioner + switch scheme { + case CONTENT_PROTECTION_ROOT_SCHEME_ID_URI: + target = &CENCContentProtection{} + case CONTENT_PROTECTION_PLAYREADY_SCHEME_ID: + target = &PlayreadyContentProtection{} + case CONTENT_PROTECTION_WIDEVINE_SCHEME_ID: + target = &WidevineContentProtection{} + default: + target = &ContentProtection{} + } + if err := d.DecodeElement(target, &start); err != nil { + return err + } + *as = append(*as, target) + return nil +} + +// wrappedAdaptationSet provides the default xml unmarshal +// to take care of the majority of our unmarshalling +type wrappedAdaptationSet AdaptationSet + +// dtoAdaptationSet parses the items out of AdaptationSet +// that give us trouble: +// * Content Protection interface +type dtoAdaptationSet struct { + wrappedAdaptationSet + ContentProtection contentProtections `xml:"ContentProtection,omitempty"` +} + +type AdaptationSet struct { + CommonAttributesAndElements + XMLName xml.Name `xml:"AdaptationSet"` + ID *string `xml:"id,attr"` + SegmentAlignment *bool `xml:"segmentAlignment,attr"` + Lang *string `xml:"lang,attr"` + Group *string `xml:"group,attr"` + PAR *string `xml:"par,attr"` + MinBandwidth *string `xml:"minBandwidth,attr"` + MaxBandwidth *string `xml:"maxBandwidth,attr"` + MinWidth *string `xml:"minWidth,attr"` + MaxWidth *string `xml:"maxWidth,attr"` + MinHeight *string `xml:"minHeight,attr"` + MaxHeight *string `xml:"maxHeight,attr"` + ContentType *string `xml:"contentType,attr"` + Roles []*Role `xml:"Role,omitempty"` + SegmentBase *SegmentBase `xml:"SegmentBase,omitempty"` + SegmentList *SegmentList `xml:"SegmentList,omitempty"` + SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate,omitempty"` // Live Profile Only + Representations []*Representation `xml:"Representation,omitempty"` + AccessibilityElems []*Accessibility `xml:"Accessibility,omitempty"` +} + +func (as *AdaptationSet) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + var n dtoAdaptationSet + if err := d.DecodeElement(&n, &start); err != nil { + return err + } + *as = AdaptationSet(n.wrappedAdaptationSet) + as.ContentProtection = make([]ContentProtectioner, len(n.ContentProtection)) + for i := range n.ContentProtection { + as.ContentProtection[i] = n.ContentProtection[i] + } + return nil +} + +// Constants for DRM / ContentProtection +const ( + CONTENT_PROTECTION_ROOT_SCHEME_ID_URI = "urn:mpeg:dash:mp4protection:2011" + CONTENT_PROTECTION_ROOT_VALUE = "cenc" + CENC_XMLNS = "urn:mpeg:cenc:2013" + CONTENT_PROTECTION_WIDEVINE_SCHEME_ID = "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" + CONTENT_PROTECTION_WIDEVINE_SCHEME_HEX = "edef8ba979d64acea3c827dcd51d21ed" + CONTENT_PROTECTION_PLAYREADY_SCHEME_ID = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95" + CONTENT_PROTECTION_PLAYREADY_SCHEME_HEX = "9a04f07998404286ab92e65be0885f95" + CONTENT_PROTECTION_PLAYREADY_SCHEME_V10_ID = "urn:uuid:79f0049a-4098-8642-ab92-e65be0885f95" + CONTENT_PROTECTION_PLAYREADY_SCHEME_V10_HEX = "79f0049a40988642ab92e65be0885f95" + CONTENT_PROTECTION_PLAYREADY_XMLNS = "urn:microsoft:playready" +) + +type ContentProtectioner interface { + ContentProtected() +} + +type ContentProtection struct { + AdaptationSet *AdaptationSet `xml:"-"` + XMLName xml.Name `xml:"ContentProtection"` + SchemeIDURI *string `xml:"schemeIdUri,attr"` // Default: urn:mpeg:dash:mp4protection:2011 + XMLNS *string `xml:"cenc,attr"` // Default: urn:mpeg:cenc:2013 + Attrs []*xml.Attr `xml:",any,attr"` +} + +type CENCContentProtection struct { + ContentProtection + DefaultKID *string `xml:"default_KID,attr"` + Value *string `xml:"value,attr"` // Default: cenc +} + +type PlayreadyContentProtection struct { + ContentProtection + PlayreadyXMLNS *string `xml:"mspr,attr,omitempty"` + PRO *string `xml:"pro,omitempty"` + PSSH *string `xml:"pssh,omitempty"` +} + +type WidevineContentProtection struct { + ContentProtection + PSSH *string `xml:"pssh,omitempty"` +} + +type ContentProtectionMarshal struct { + AdaptationSet *AdaptationSet `xml:"-"` + XMLName xml.Name `xml:"ContentProtection"` + SchemeIDURI *string `xml:"schemeIdUri,attr"` // Default: urn:mpeg:dash:mp4protection:2011 + XMLNS *string `xml:"xmlns:cenc,attr"` // Default: urn:mpeg:cenc:2013 + Attrs []*xml.Attr `xml:",any,attr"` +} + +type CENCContentProtectionMarshal struct { + ContentProtectionMarshal + DefaultKID *string `xml:"cenc:default_KID,attr"` + Value *string `xml:"value,attr"` // Default: cenc +} + +type PlayreadyContentProtectionMarshal struct { + ContentProtectionMarshal + PlayreadyXMLNS *string `xml:"xmlns:mspr,attr,omitempty"` + PRO *string `xml:"mspr:pro,omitempty"` + PSSH *string `xml:"cenc:pssh,omitempty"` +} + +type WidevineContentProtectionMarshal struct { + ContentProtectionMarshal + PSSH *string `xml:"cenc:pssh,omitempty"` +} + +func (s ContentProtection) ContentProtected() {} + +func (s ContentProtection) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + err := e.Encode(&ContentProtectionMarshal{ + s.AdaptationSet, + s.XMLName, + s.SchemeIDURI, + s.XMLNS, + s.Attrs, + }) + if err != nil { + return err + } + return nil +} + +func (s CENCContentProtection) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + err := e.Encode(&CENCContentProtectionMarshal{ + ContentProtectionMarshal{ + s.AdaptationSet, + s.XMLName, + s.SchemeIDURI, + s.XMLNS, + s.Attrs, + }, + s.DefaultKID, + s.Value, + }) + if err != nil { + return err + } + return nil +} + +func (s PlayreadyContentProtection) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + err := e.Encode(&PlayreadyContentProtectionMarshal{ + ContentProtectionMarshal{ + s.AdaptationSet, + s.XMLName, + s.SchemeIDURI, + s.XMLNS, + s.Attrs, + }, + s.PlayreadyXMLNS, + s.PRO, + s.PSSH, + }) + if err != nil { + return err + } + return nil +} + +func (s WidevineContentProtection) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + err := e.Encode(&WidevineContentProtectionMarshal{ + ContentProtectionMarshal{ + s.AdaptationSet, + s.XMLName, + s.SchemeIDURI, + s.XMLNS, + s.Attrs, + }, + s.PSSH, + }) + if err != nil { + return err + } + return nil +} + +type Role struct { + AdaptationSet *AdaptationSet `xml:"-"` + SchemeIDURI *string `xml:"schemeIdUri,attr"` + Value *string `xml:"value,attr"` +} + +// Segment Template is for Live Profile Only +type SegmentTemplate struct { + AdaptationSet *AdaptationSet `xml:"-"` + SegmentTimeline *SegmentTimeline `xml:"SegmentTimeline,omitempty"` + PresentationTimeOffset *uint64 `xml:"presentationTimeOffset,attr,omitempty"` + Duration *int64 `xml:"duration,attr"` + Initialization *string `xml:"initialization,attr"` + Media *string `xml:"media,attr"` + StartNumber *int64 `xml:"startNumber,attr"` + Timescale *int64 `xml:"timescale,attr"` +} + +type Representation struct { + CommonAttributesAndElements + AdaptationSet *AdaptationSet `xml:"-"` + AudioChannelConfiguration *AudioChannelConfiguration `xml:"AudioChannelConfiguration,omitempty"` + AudioSamplingRate *int64 `xml:"audioSamplingRate,attr"` // Audio + Bandwidth *int64 `xml:"bandwidth,attr"` // Audio + Video + Codecs *string `xml:"codecs,attr"` // Audio + Video + FrameRate *string `xml:"frameRate,attr,omitempty"` // Video + Height *int64 `xml:"height,attr"` // Video + ID *string `xml:"id,attr"` // Audio + Video + Width *int64 `xml:"width,attr"` // Video + BaseURL *string `xml:"BaseURL,omitempty"` // On-Demand Profile + SegmentBase *SegmentBase `xml:"SegmentBase,omitempty"` // On-Demand Profile + SegmentList *SegmentList `xml:"SegmentList,omitempty"` + SegmentTemplate *SegmentTemplate `xml:"SegmentTemplate,omitempty"` +} + +type Accessibility struct { + AdaptationSet *AdaptationSet `xml:"-"` + SchemeIdUri *string `xml:"schemeIdUri,attr,omitempty"` + Value *string `xml:"value,attr,omitempty"` +} + +type AudioChannelConfiguration struct { + SchemeIDURI *string `xml:"schemeIdUri,attr"` + // Value will be an int for non-Dolby Schemes, and a hexstring for Dolby Schemes, hence we make it a string + Value *string `xml:"value,attr"` +} + +// Creates a new static MPD object. +// profile - DASH Profile (Live or OnDemand). +// mediaPresentationDuration - Media Presentation Duration (i.e. PT6M16S). +// minBufferTime - Min Buffer Time (i.e. PT1.97S). +// attributes - Other attributes (optional). +func NewMPD(profile DashProfile, mediaPresentationDuration, minBufferTime string, attributes ...AttrMPD) *MPD { + period := &Period{} + mpd := &MPD{ + XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), + Profiles: Strptr((string)(profile)), + Type: Strptr("static"), + MediaPresentationDuration: Strptr(mediaPresentationDuration), + MinBufferTime: Strptr(minBufferTime), + period: period, + Periods: []*Period{period}, + } + + for i := range attributes { + switch attr := attributes[i].(type) { + case *attrAvailabilityStartTime: + mpd.AvailabilityStartTime = attr.GetStrptr() + } + } + + return mpd +} + +// Creates a new dynamic MPD object. +// profile - DASH Profile (Live or OnDemand). +// availabilityStartTime - anchor for the computation of the earliest availability time (in UTC). +// minBufferTime - Min Buffer Time (i.e. PT1.97S). +// attributes - Other attributes (optional). +func NewDynamicMPD(profile DashProfile, availabilityStartTime, minBufferTime string, attributes ...AttrMPD) *MPD { + period := &Period{} + mpd := &MPD{ + XMLNs: Strptr("urn:mpeg:dash:schema:mpd:2011"), + Profiles: Strptr((string)(profile)), + Type: Strptr("dynamic"), + AvailabilityStartTime: Strptr(availabilityStartTime), + MinBufferTime: Strptr(minBufferTime), + period: period, + Periods: []*Period{period}, + UTCTiming: &DescriptorType{}, + } + + for i := range attributes { + switch attr := attributes[i].(type) { + case *attrMinimumUpdatePeriod: + mpd.MinimumUpdatePeriod = attr.GetStrptr() + case *attrMediaPresentationDuration: + mpd.MediaPresentationDuration = attr.GetStrptr() + case *attrPublishTime: + mpd.PublishTime = attr.GetStrptr() + } + } + + return mpd +} + +// AddNewPeriod creates a new Period and make it the currently active one. +func (m *MPD) AddNewPeriod() *Period { + if m.period != nil && m.period.ID == "" && m.period.AdaptationSets == nil { + return m.GetCurrentPeriod() + } + period := &Period{} + m.Periods = append(m.Periods, period) + m.period = period + return period +} + +// GetCurrentPeriod returns the current Period. +func (m *MPD) GetCurrentPeriod() *Period { + return m.period +} + +func (period *Period) SetDuration(d time.Duration) { + period.Duration = Duration(d) +} + +// Create a new Adaptation Set for thumbnails. +// mimeType - e.g. (image/jpeg) +func (m *MPD) AddNewAdaptationSetThumbnails(mimeType string) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetThumbnails(mimeType) +} + +func (period *Period) AddNewAdaptationSetThumbnails(mimeType string) (*AdaptationSet, error) { + as := &AdaptationSet{ + ContentType: Strptr(DASH_CONTENT_TYPE_IMAGE), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +func (m *MPD) AddNewAdaptationSetThumbnailsWithID(id, mimeType string) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetThumbnailsWithID(id, mimeType) +} + +func (period *Period) AddNewAdaptationSetThumbnailsWithID(id, mimeType string) (*AdaptationSet, error) { + as := &AdaptationSet{ + ID: Strptr(id), + ContentType: Strptr(DASH_CONTENT_TYPE_IMAGE), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Create a new Adaptation Set for Audio Assets. +// mimeType - MIME Type (i.e. audio/mp4). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +// lang - Language (i.e. en). +func (m *MPD) AddNewAdaptationSetAudio(mimeType string, segmentAlignment bool, startWithSAP int64, lang string) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetAudio(mimeType, segmentAlignment, startWithSAP, lang) +} + +// Create a new Adaptation Set for Audio Assets. +// mimeType - MIME Type (i.e. audio/mp4). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +// lang - Language (i.e. en). +func (m *MPD) AddNewAdaptationSetAudioWithID(id string, mimeType string, segmentAlignment bool, startWithSAP int64, lang string) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetAudioWithID(id, mimeType, segmentAlignment, startWithSAP, lang) +} + +// Create a new Adaptation Set for Audio Assets. +// mimeType - MIME Type (i.e. audio/mp4). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +// lang - Language (i.e. en). +func (period *Period) AddNewAdaptationSetAudio(mimeType string, segmentAlignment bool, startWithSAP int64, lang string) (*AdaptationSet, error) { + as := &AdaptationSet{ + SegmentAlignment: Boolptr(segmentAlignment), + Lang: Strptr(lang), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + StartWithSAP: Int64ptr(startWithSAP), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Create a new Adaptation Set for Audio Assets. +// mimeType - MIME Type (i.e. audio/mp4). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +// lang - Language (i.e. en). +func (period *Period) AddNewAdaptationSetAudioWithID(id string, mimeType string, segmentAlignment bool, startWithSAP int64, lang string) (*AdaptationSet, error) { + as := &AdaptationSet{ + ID: Strptr(id), + SegmentAlignment: Boolptr(segmentAlignment), + Lang: Strptr(lang), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + StartWithSAP: Int64ptr(startWithSAP), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Create a new Adaptation Set for Video Assets. +// mimeType - MIME Type (i.e. video/mp4). +// scanType - Scan Type (i.e.progressive). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +func (m *MPD) AddNewAdaptationSetVideo(mimeType string, scanType string, segmentAlignment bool, startWithSAP int64) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetVideo(mimeType, scanType, segmentAlignment, startWithSAP) +} + +// Create a new Adaptation Set for Video Assets. +// mimeType - MIME Type (i.e. video/mp4). +// scanType - Scan Type (i.e.progressive). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +func (m *MPD) AddNewAdaptationSetVideoWithID(id string, mimeType string, scanType string, segmentAlignment bool, startWithSAP int64) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetVideoWithID(id, mimeType, scanType, segmentAlignment, startWithSAP) +} + +// Create a new Adaptation Set for Video Assets. +// mimeType - MIME Type (i.e. video/mp4). +// scanType - Scan Type (i.e.progressive). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +func (period *Period) AddNewAdaptationSetVideo(mimeType string, scanType string, segmentAlignment bool, startWithSAP int64) (*AdaptationSet, error) { + as := &AdaptationSet{ + SegmentAlignment: Boolptr(segmentAlignment), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + StartWithSAP: Int64ptr(startWithSAP), + ScanType: Strptr(scanType), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Create a new Adaptation Set for Video Assets. +// mimeType - MIME Type (i.e. video/mp4). +// scanType - Scan Type (i.e.progressive). +// segmentAlignment - Segment Alignment(i.e. true). +// startWithSAP - Starts With SAP (i.e. 1). +func (period *Period) AddNewAdaptationSetVideoWithID(id string, mimeType string, scanType string, segmentAlignment bool, startWithSAP int64) (*AdaptationSet, error) { + as := &AdaptationSet{ + SegmentAlignment: Boolptr(segmentAlignment), + ID: Strptr(id), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + StartWithSAP: Int64ptr(startWithSAP), + ScanType: Strptr(scanType), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Create a new Adaptation Set for Subtitle Assets. +// mimeType - MIME Type (i.e. text/vtt). +// lang - Language (i.e. en). +func (m *MPD) AddNewAdaptationSetSubtitle(mimeType string, lang string) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetSubtitle(mimeType, lang) +} + +// Create a new Adaptation Set for Subtitle Assets. +// mimeType - MIME Type (i.e. text/vtt). +// lang - Language (i.e. en). +func (m *MPD) AddNewAdaptationSetSubtitleWithID(id string, mimeType string, lang string) (*AdaptationSet, error) { + return m.period.AddNewAdaptationSetSubtitleWithID(id, mimeType, lang) +} + +// Create a new Adaptation Set for Subtitle Assets. +// mimeType - MIME Type (i.e. text/vtt). +// lang - Language (i.e. en). +func (period *Period) AddNewAdaptationSetSubtitle(mimeType string, lang string) (*AdaptationSet, error) { + as := &AdaptationSet{ + Lang: Strptr(lang), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Create a new Adaptation Set for Subtitle Assets. +// mimeType - MIME Type (i.e. text/vtt). +// lang - Language (i.e. en). +func (period *Period) AddNewAdaptationSetSubtitleWithID(id string, mimeType string, lang string) (*AdaptationSet, error) { + as := &AdaptationSet{ + ID: Strptr(id), + Lang: Strptr(lang), + CommonAttributesAndElements: CommonAttributesAndElements{ + MimeType: Strptr(mimeType), + }, + } + err := period.addAdaptationSet(as) + if err != nil { + return nil, err + } + return as, nil +} + +// Internal helper method for adding a AdapatationSet. +func (period *Period) addAdaptationSet(as *AdaptationSet) error { + if as == nil { + return ErrAdaptationSetNil + } + period.AdaptationSets = append(period.AdaptationSets, as) + return nil +} + +// Adds a ContentProtection tag at the root level of an AdaptationSet. +// This ContentProtection tag does not include signaling for any particular DRM scheme. +// defaultKIDHex - Default Key ID as a Hex String. +// +// NOTE: this is only here for Legacy purposes. This will create an invalid UUID. +func (as *AdaptationSet) AddNewContentProtectionRootLegacyUUID(defaultKIDHex string) (*CENCContentProtection, error) { + if len(defaultKIDHex) != 32 || defaultKIDHex == "" { + return nil, ErrInvalidDefaultKID + } + + // Convert the KID into the correct format + defaultKID := strings.ToLower(defaultKIDHex[0:8] + "-" + defaultKIDHex[8:12] + "-" + defaultKIDHex[12:16] + "-" + defaultKIDHex[16:32]) + + cp := &CENCContentProtection{ + DefaultKID: Strptr(defaultKID), + Value: Strptr(CONTENT_PROTECTION_ROOT_VALUE), + } + cp.SchemeIDURI = Strptr(CONTENT_PROTECTION_ROOT_SCHEME_ID_URI) + cp.XMLNS = Strptr(CENC_XMLNS) + + err := as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +// Adds a ContentProtection tag at the root level of an AdaptationSet. +// This ContentProtection tag does not include signaling for any particular DRM scheme. +// defaultKIDHex - Default Key ID as a Hex String. +func (as *AdaptationSet) AddNewContentProtectionRoot(defaultKIDHex string) (*CENCContentProtection, error) { + if len(defaultKIDHex) != 32 || defaultKIDHex == "" { + return nil, ErrInvalidDefaultKID + } + + // Convert the KID into the correct format + defaultKID := strings.ToLower(defaultKIDHex[0:8] + "-" + defaultKIDHex[8:12] + "-" + defaultKIDHex[12:16] + "-" + defaultKIDHex[16:20] + "-" + defaultKIDHex[20:32]) + + cp := &CENCContentProtection{ + DefaultKID: Strptr(defaultKID), + Value: Strptr(CONTENT_PROTECTION_ROOT_VALUE), + } + cp.SchemeIDURI = Strptr(CONTENT_PROTECTION_ROOT_SCHEME_ID_URI) + cp.XMLNS = Strptr(CENC_XMLNS) + + err := as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +// AddNewContentProtectionSchemeWidevine adds a new content protection scheme for Widevine DRM to the adaptation set. With +// a element that contains a Base64 encoded PSSH box +// wvHeader - binary representation of Widevine Header +// !!! Note: this function will accept any byte slice as a wvHeader value !!! +func (as *AdaptationSet) AddNewContentProtectionSchemeWidevineWithPSSH(wvHeader []byte) (*WidevineContentProtection, error) { + cp, err := NewWidevineContentProtection(wvHeader) + if err != nil { + return nil, err + } + + err = as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +// AddNewContentProtectionSchemeWidevine adds a new content protection scheme for Widevine DRM to the adaptation set. +func (as *AdaptationSet) AddNewContentProtectionSchemeWidevine() (*WidevineContentProtection, error) { + cp, err := NewWidevineContentProtection(nil) + if err != nil { + return nil, err + } + + err = as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +func NewWidevineContentProtection(wvHeader []byte) (*WidevineContentProtection, error) { + cp := &WidevineContentProtection{} + cp.SchemeIDURI = Strptr(CONTENT_PROTECTION_WIDEVINE_SCHEME_ID) + + if len(wvHeader) > 0 { + cp.XMLNS = Strptr(CENC_XMLNS) + wvSystemID, err := hex.DecodeString(CONTENT_PROTECTION_WIDEVINE_SCHEME_HEX) + if err != nil { + panic(err.Error()) + } + psshBox, err := MakePSSHBox(wvSystemID, wvHeader) + if err != nil { + return nil, err + } + + psshB64 := base64.StdEncoding.EncodeToString(psshBox) + cp.PSSH = &psshB64 + } + return cp, nil +} + +// AddNewContentProtectionSchemePlayready adds a new content protection scheme for PlayReady DRM. +// pro - PlayReady Object Header, as a Base64 encoded string. +func (as *AdaptationSet) AddNewContentProtectionSchemePlayready(pro string) (*PlayreadyContentProtection, error) { + cp, err := newPlayreadyContentProtection(pro, CONTENT_PROTECTION_PLAYREADY_SCHEME_ID) + if err != nil { + return nil, err + } + + err = as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +// AddNewContentProtectionSchemePlayreadyV10 adds a new content protection scheme for PlayReady v1.0 DRM. +// pro - PlayReady Object Header, as a Base64 encoded string. +func (as *AdaptationSet) AddNewContentProtectionSchemePlayreadyV10(pro string) (*PlayreadyContentProtection, error) { + cp, err := newPlayreadyContentProtection(pro, CONTENT_PROTECTION_PLAYREADY_SCHEME_V10_ID) + if err != nil { + return nil, err + } + + err = as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +func newPlayreadyContentProtection(pro string, schemeIDURI string) (*PlayreadyContentProtection, error) { + if pro == "" { + return nil, ErrPROEmpty + } + + cp := &PlayreadyContentProtection{ + PlayreadyXMLNS: Strptr(CONTENT_PROTECTION_PLAYREADY_XMLNS), + PRO: Strptr(pro), + } + cp.SchemeIDURI = Strptr(schemeIDURI) + + return cp, nil +} + +// AddNewContentProtectionSchemePlayreadyWithPSSH adds a new content protection scheme for PlayReady DRM. The scheme +// will include both ms:pro and cenc:pssh subelements +// pro - PlayReady Object Header, as a Base64 encoded string. +func (as *AdaptationSet) AddNewContentProtectionSchemePlayreadyWithPSSH(pro string) (*PlayreadyContentProtection, error) { + cp, err := newPlayreadyContentProtection(pro, CONTENT_PROTECTION_PLAYREADY_SCHEME_ID) + if err != nil { + return nil, err + } + cp.XMLNS = Strptr(CENC_XMLNS) + prSystemID, err := hex.DecodeString(CONTENT_PROTECTION_PLAYREADY_SCHEME_HEX) + if err != nil { + panic(err.Error()) + } + + proBin, err := base64.StdEncoding.DecodeString(pro) + if err != nil { + return nil, err + } + + psshBox, err := MakePSSHBox(prSystemID, proBin) + if err != nil { + return nil, err + } + cp.PSSH = Strptr(base64.StdEncoding.EncodeToString(psshBox)) + + err = as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +// AddNewContentProtectionSchemePlayreadyV10WithPSSH adds a new content protection scheme for PlayReady v1.0 DRM. The scheme +// will include both ms:pro and cenc:pssh subelements +// pro - PlayReady Object Header, as a Base64 encoded string. +func (as *AdaptationSet) AddNewContentProtectionSchemePlayreadyV10WithPSSH(pro string) (*PlayreadyContentProtection, error) { + cp, err := newPlayreadyContentProtection(pro, CONTENT_PROTECTION_PLAYREADY_SCHEME_V10_ID) + if err != nil { + return nil, err + } + cp.XMLNS = Strptr(CENC_XMLNS) + prSystemID, err := hex.DecodeString(CONTENT_PROTECTION_PLAYREADY_SCHEME_V10_HEX) + if err != nil { + panic(err.Error()) + } + + proBin, err := base64.StdEncoding.DecodeString(pro) + if err != nil { + return nil, err + } + + psshBox, err := MakePSSHBox(prSystemID, proBin) + if err != nil { + return nil, err + } + cp.PSSH = Strptr(base64.StdEncoding.EncodeToString(psshBox)) + + err = as.AddContentProtection(cp) + if err != nil { + return nil, err + } + return cp, nil +} + +// Internal helper method for adding a ContentProtection to an AdaptationSet. +func (as *AdaptationSet) AddContentProtection(cp ContentProtectioner) error { + if cp == nil { + return ErrContentProtectionNil + } + + as.ContentProtection = append(as.ContentProtection, cp) + return nil +} + +// Sets up a new SegmentTemplate for an AdaptationSet. +// duration - relative to timescale (i.e. 2000). +// init - template string for init segment (i.e. $RepresentationID$/audio/en/init.mp4). +// media - template string for media segments. +// startNumber - the number to start segments from ($Number$) (i.e. 0). +// timescale - sets the timescale for duration (i.e. 1000, represents milliseconds). +func (as *AdaptationSet) SetNewSegmentTemplate(duration int64, init string, media string, startNumber int64, timescale int64) (*SegmentTemplate, error) { + st := &SegmentTemplate{ + Duration: Int64ptr(duration), + Initialization: Strptr(init), + Media: Strptr(media), + StartNumber: Int64ptr(startNumber), + Timescale: Int64ptr(timescale), + } + + err := as.setSegmentTemplate(st) + if err != nil { + return nil, err + } + return st, nil +} + +// Internal helper method for setting the Segment Template on an AdaptationSet. +func (as *AdaptationSet) setSegmentTemplate(st *SegmentTemplate) error { + if st == nil { + return ErrSegmentTemplateNil + } + st.AdaptationSet = as + as.SegmentTemplate = st + return nil +} + +// Adds a new SegmentTemplate to a thumbnail AdaptationSet +// duration - relative to timescale (i.e. 2000). +// media - template string for media segments. +// startNumber - the number to start segments from ($Number$) (i.e. 0). +// timescale - sets the timescale for duration (i.e. 1000, represents milliseconds). +func (as *AdaptationSet) SetNewSegmentTemplateThumbnails(duration int64, media string, startNumber int64, timescale int64) (*SegmentTemplate, error) { + st := &SegmentTemplate{ + Duration: Int64ptr(duration), + Media: Strptr(media), + StartNumber: Int64ptr(startNumber), + Timescale: Int64ptr(timescale), + } + + err := as.setSegmentTemplate(st) + if err != nil { + return nil, err + } + return st, nil +} + +// Adds a new Thumbnail representation to an AdaptationSet. +// bandwidth - in Bits/s (i.e. 1518664). +// id - ID for this representation, will get used as $RepresentationID$ in template strings. +// width - width of the video (i.e. 1280). +// height - height of the video (i.e 720). +// uri - +func (as *AdaptationSet) AddNewRepresentationThumbnails(id, val, uri string, bandwidth, width, height int64) (*Representation, error) { + r := &Representation{ + Bandwidth: Int64ptr(bandwidth), + ID: Strptr(id), + Width: Int64ptr(width), + Height: Int64ptr(height), + CommonAttributesAndElements: CommonAttributesAndElements{ + EssentialProperty: []DescriptorType{ + { + SchemeIDURI: Strptr(uri), + Value: Strptr(val), + }, + }, + }, + } + + err := as.addRepresentation(r) + if err != nil { + return nil, err + } + return r, nil +} + +// Adds a new Audio representation to an AdaptationSet. +// samplingRate - in Hz (i.e. 44100). +// bandwidth - in Bits/s (i.e. 67095). +// codecs - codec string for Audio Only (in RFC6381, https://tools.ietf.org/html/rfc6381) (i.e. mp4a.40.2). +// id - ID for this representation, will get used as $RepresentationID$ in template strings. +func (as *AdaptationSet) AddNewRepresentationAudio(samplingRate int64, bandwidth int64, codecs string, id string) (*Representation, error) { + r := &Representation{ + AudioSamplingRate: Int64ptr(samplingRate), + Bandwidth: Int64ptr(bandwidth), + Codecs: Strptr(codecs), + ID: Strptr(id), + } + + err := as.addRepresentation(r) + if err != nil { + return nil, err + } + return r, nil +} + +// Adds a new Video representation to an AdaptationSet. +// bandwidth - in Bits/s (i.e. 1518664). +// codecs - codec string for Audio Only (in RFC6381, https://tools.ietf.org/html/rfc6381) (i.e. avc1.4d401f). +// id - ID for this representation, will get used as $RepresentationID$ in template strings. +// frameRate - video frame rate (as a fraction) (i.e. 30000/1001). +// width - width of the video (i.e. 1280). +// height - height of the video (i.e 720). +func (as *AdaptationSet) AddNewRepresentationVideo(bandwidth int64, codecs string, id string, frameRate string, width int64, height int64) (*Representation, error) { + r := &Representation{ + Bandwidth: Int64ptr(bandwidth), + Codecs: Strptr(codecs), + ID: Strptr(id), + FrameRate: Strptr(frameRate), + Width: Int64ptr(width), + Height: Int64ptr(height), + } + + err := as.addRepresentation(r) + if err != nil { + return nil, err + } + return r, nil +} + +// Adds a new Subtitle representation to an AdaptationSet. +// bandwidth - in Bits/s (i.e. 256). +// id - ID for this representation, will get used as $RepresentationID$ in template strings. +func (as *AdaptationSet) AddNewRepresentationSubtitle(bandwidth int64, id string) (*Representation, error) { + r := &Representation{ + Bandwidth: Int64ptr(bandwidth), + ID: Strptr(id), + } + + err := as.addRepresentation(r) + if err != nil { + return nil, err + } + return r, nil +} + +// Internal helper method for adding a Representation to an AdaptationSet. +func (as *AdaptationSet) addRepresentation(r *Representation) error { + if r == nil { + return ErrRepresentationNil + } + r.AdaptationSet = as + as.Representations = append(as.Representations, r) + return nil +} + +// Internal helper method for adding an Accessibility element to an AdaptationSet. +func (as *AdaptationSet) addAccessibility(a *Accessibility) error { + if a == nil { + return ErrAccessibilityNil + } + a.AdaptationSet = as + as.AccessibilityElems = append(as.AccessibilityElems, a) + return nil +} + +// Adds a new Role to an AdaptationSet +// schemeIdUri - Scheme ID URI string (i.e. urn:mpeg:dash:role:2011) +// value - Value for this role, (i.e. caption, subtitle, main, alternate, supplementary, commentary, dub) +func (as *AdaptationSet) AddNewRole(schemeIDURI string, value string) (*Role, error) { + r := &Role{ + SchemeIDURI: Strptr(schemeIDURI), + Value: Strptr(value), + } + r.AdaptationSet = as + as.Roles = append(as.Roles, r) + return r, nil +} + +// AddNewAccessibilityElement adds a new accessibility element to an adaptation set +// schemeIdUri - Scheme ID URI for the Accessibility element (i.e. urn:tva:metadata:cs:AudioPurposeCS:2007) +// value - specified value based on scheme +func (as *AdaptationSet) AddNewAccessibilityElement(scheme AccessibilityElementScheme, val string) (*Accessibility, error) { + accessibility := &Accessibility{ + SchemeIdUri: Strptr((string)(scheme)), + Value: Strptr(val), + } + + err := as.addAccessibility(accessibility) + if err != nil { + return nil, err + } + + return accessibility, nil +} + +// Sets the BaseURL for a Representation. +// baseURL - Base URL as a string (i.e. 800k/output-audio-und.mp4) +func (r *Representation) SetNewBaseURL(baseURL string) error { + if baseURL == "" { + return ErrBaseURLEmpty + } + r.BaseURL = Strptr(baseURL) + return nil +} + +// Sets a new SegmentBase on a Representation. +// This is for On Demand profile. +// indexRange - Byte range to the index (sidx)atom. +// init - Byte range to the init atoms (ftyp+moov). +func (r *Representation) AddNewSegmentBase(indexRange string, initRange string) (*SegmentBase, error) { + sb := &SegmentBase{ + IndexRange: Strptr(indexRange), + Initialization: &URL{Range: Strptr(initRange)}, + } + + err := r.setSegmentBase(sb) + if err != nil { + return nil, err + } + return sb, nil +} + +// Internal helper method for setting the SegmentBase on a Representation. +func (r *Representation) setSegmentBase(sb *SegmentBase) error { + if r.AdaptationSet == nil { + return ErrNoDASHProfileSet + } + if sb == nil { + return ErrSegmentBaseNil + } + r.SegmentBase = sb + return nil +} + +// Sets a new AudioChannelConfiguration on a Representation. +// This is required for the HbbTV profile. +// scheme - One of the two AudioConfigurationSchemes. +// channelConfiguration - string that represents the channel configuration. +func (r *Representation) AddNewAudioChannelConfiguration(scheme AudioChannelConfigurationScheme, channelConfiguration string) (*AudioChannelConfiguration, error) { + acc := &AudioChannelConfiguration{ + SchemeIDURI: Strptr((string)(scheme)), + Value: Strptr(channelConfiguration), + } + + err := r.setAudioChannelConfiguration(acc) + if err != nil { + return nil, err + } + + return acc, nil +} + +// Internal helper method for setting the SegmentBase on a Representation. +func (r *Representation) setAudioChannelConfiguration(acc *AudioChannelConfiguration) error { + if acc == nil { + return ErrAudioChannelConfigurationNil + } + r.AudioChannelConfiguration = acc + return nil +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/mpd_attr.go b/vendor/github.com/zencoder/go-dash/v3/mpd/mpd_attr.go new file mode 100644 index 000000000..c64514a71 --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/mpd_attr.go @@ -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} +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/mpd_read_write.go b/vendor/github.com/zencoder/go-dash/v3/mpd/mpd_read_write.go new file mode 100644 index 000000000..35d3f9f33 --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/mpd_read_write.go @@ -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 +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/pssh.go b/vendor/github.com/zencoder/go-dash/v3/mpd/pssh.go new file mode 100644 index 000000000..98a7112c2 --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/pssh.go @@ -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 +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/segment.go b/vendor/github.com/zencoder/go-dash/v3/mpd/segment.go new file mode 100644 index 000000000..bb112bfc7 --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/segment.go @@ -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"` +} diff --git a/vendor/github.com/zencoder/go-dash/v3/mpd/validate.go b/vendor/github.com/zencoder/go-dash/v3/mpd/validate.go new file mode 100644 index 000000000..6c8521acb --- /dev/null +++ b/vendor/github.com/zencoder/go-dash/v3/mpd/validate.go @@ -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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1b16762d3..bc780c6a5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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