diff --git a/handlers/schemas/UploadVOD.yaml b/handlers/schemas/UploadVOD.yaml index 7b2f79307..a217f5bfb 100644 --- a/handlers/schemas/UploadVOD.yaml +++ b/handlers/schemas/UploadVOD.yaml @@ -24,6 +24,16 @@ properties: required: - "encrypted_key" additionalProperties: false + clip_strategy: + type: "object" + properties: + start_time: + type: "integer" + end_time: + type: "integer" + required: + - "start_time" + additionalProperties: false pipeline_strategy: type: string description: diff --git a/handlers/upload.go b/handlers/upload.go index dc7777e39..c9c3ceb2e 100644 --- a/handlers/upload.go +++ b/handlers/upload.go @@ -49,6 +49,9 @@ type UploadVODRequest struct { TargetSegmentSizeSecs int64 `json:"target_segment_size_secs"` Profiles []video.EncodedProfile `json:"profiles"` PipelineStrategy pipeline.Strategy `json:"pipeline_strategy"` + + // Forwarded to clipping stage: + ClipStrategy video.ClipStrategy `json:"clip_strategy"` } type UploadVODResponse struct { @@ -101,6 +104,15 @@ func (r UploadVODRequest) IsProfileValid() bool { return true } +func (r UploadVODRequest) IsClipValid() bool { + startTime := r.ClipStrategy.StartTime + endTime := r.ClipStrategy.EndTime + if startTime < 0 || endTime <= 0 || startTime == endTime || startTime < endTime { + return false + } + return true +} + func (r UploadVODRequest) getTargetHlsOutput() UploadVODRequestOutputLocation { for _, o := range r.OutputLocations { if o.Outputs.HLS == "enabled" { @@ -196,6 +208,12 @@ func (d *CatalystAPIHandlersCollection) handleUploadVOD(w http.ResponseWriter, r } log.AddContext(requestID, "target_segment_size_secs", uploadVODRequest.TargetSegmentSizeSecs) + // Check if this is a clipping request + if uploadVODRequest.IsClipValid() { + uploadVODRequest.ClipStrategy.Enabled = true + } + + // Get target locatons for HLS, MP4, FMP4 outputs hlsTargetOutput := uploadVODRequest.getTargetHlsOutput() hlsTargetURL, err := toTargetURL(hlsTargetOutput, requestID) if err != nil { @@ -211,11 +229,11 @@ func (d *CatalystAPIHandlersCollection) handleUploadVOD(w http.ResponseWriter, r if err != nil { return false, errors.WriteHTTPBadRequest(w, "Invalid request payload", err) } - if hlsTargetURL == nil && mp4TargetURL == nil && fragMp4TargetURL == nil { return false, errors.WriteHTTPBadRequest(w, "Invalid request payload", errors2.New("none of output enabled: hls or mp4 or f-mp4")) } + // Verify pipeline strategy if strat := uploadVODRequest.PipelineStrategy; strat != "" && !strat.IsValid() { return false, errors.WriteHTTPBadRequest(w, "Invalid request payload", fmt.Errorf("invalid value provided for pipeline strategy: %q", uploadVODRequest.PipelineStrategy)) } @@ -224,7 +242,6 @@ func (d *CatalystAPIHandlersCollection) handleUploadVOD(w http.ResponseWriter, r // Once we're happy with the request, do the rest of the Segmenting stage asynchronously to allow us to // from the API call and free up the HTTP connection - d.VODEngine.StartUploadJob(pipeline.UploadJobPayload{ SourceFile: uploadVODRequest.Url, CallbackURL: uploadVODRequest.CallbackUrl, @@ -241,6 +258,7 @@ func (d *CatalystAPIHandlersCollection) handleUploadVOD(w http.ResponseWriter, r TargetSegmentSizeSecs: uploadVODRequest.TargetSegmentSizeSecs, Encryption: uploadVODRequest.Encryption, SourceCopy: uploadVODRequest.getSourceCopyEnabled(), + ClipStrategy: uploadVODRequest.ClipStrategy, }) respBytes, err := json.Marshal(UploadVODResponse{RequestID: requestID}) diff --git a/pipeline/coordinator.go b/pipeline/coordinator.go index 8ea58ce13..2caac9bbf 100644 --- a/pipeline/coordinator.go +++ b/pipeline/coordinator.go @@ -74,6 +74,7 @@ type UploadJobPayload struct { Encryption *EncryptionPayload InputFileInfo video.InputVideo SourceCopy bool + ClipStrategy video.ClipStrategy } type EncryptionPayload struct { @@ -273,6 +274,15 @@ func (c *Coordinator) StartUploadJob(p UploadJobPayload) { osTransferURL := c.SourceOutputURL.JoinPath(p.RequestID, "transfer", path.Base(sourceURL.Path)) if clients.IsHLSInput(sourceURL) { + // Currently we only clip an HLS source (e.g recordings or transcoded asset) + if p.ClipStrategy.Enabled { + log.Log(p.RequestID, "clippity clipping the input") + // Use new clipped manifest as the source URL + sourceURL, err = video.ClipInput(p.RequestID, sourceURL, p.ClipStrategy.StartTime, p.ClipStrategy.EndTime) + if err != nil { + return nil, fmt.Errorf("error clipping input: %w", err) + } + } osTransferURL = sourceURL } else if p.SourceCopy { log.Log(p.RequestID, "source copy enabled") diff --git a/video/clip.go b/video/clip.go index f0aeb5cc1..7f0ee9511 100644 --- a/video/clip.go +++ b/video/clip.go @@ -5,9 +5,16 @@ import ( "github.com/grafov/m3u8" "github.com/livepeer/catalyst-api/log" ffmpeg "github.com/u2takey/ffmpeg-go" + "net/url" "time" ) +type ClipStrategy struct { + Enabled bool + StartTime float64 `json:"start_time,omitempty"` + EndTime float64 `json:"end_time,omitempty"` +} + // format time in secs to be copatible with ffmpeg's expected time syntax func formatTime(seconds float64) string { duration := time.Duration(seconds * float64(time.Second)) @@ -55,6 +62,13 @@ func getRelevantSegment(allSegments []*m3u8.MediaSegment, playHeadTime float64, return 0, fmt.Errorf("error clipping: did not find a segment that falls within %v seconds", playHeadTime) } +// Function that will take a source URL manifest and return a new URL +// pointing to the clipped manifest +func ClipInput(requestID string, srcUrl *url.URL, startTime, endTime float64) (*url.URL, error) { + // TODO:*actually* do the clipping + return srcUrl, nil +} + // Function to find relevant segments that span from the clipping start and end times func ClipManifest(requestID string, manifest *m3u8.MediaPlaylist, startTime, endTime float64) ([]*m3u8.MediaSegment, error) { var startSegIdx, endSegIdx uint64