mirror of
https://github.com/owncast/owncast.git
synced 2024-12-18 07:12:33 +03:00
143 lines
4.7 KiB
Go
143 lines
4.7 KiB
Go
|
package replays
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/grafov/m3u8"
|
||
|
"github.com/owncast/owncast/db"
|
||
|
"github.com/owncast/owncast/models"
|
||
|
"github.com/pkg/errors"
|
||
|
)
|
||
|
|
||
|
// GenerateMasterPlaylistForClip returns a master playlist for a given clip Id.
|
||
|
// It includes references to the media playlists for each output configuration.
|
||
|
func (p *PlaylistGenerator) GenerateMasterPlaylistForClip(clipId string) (*m3u8.MasterPlaylist, error) {
|
||
|
clip, err := p.datastore.GetQueries().GetClip(context.Background(), clipId)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "unable to fetch requested clip")
|
||
|
}
|
||
|
|
||
|
streamId := clip.StreamID
|
||
|
configs, err := p.GetConfigurationsForStream(streamId)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to get configurations for stream")
|
||
|
}
|
||
|
|
||
|
// Create the master playlist that will hold the different media playlists.
|
||
|
masterPlaylist := p.createNewMasterPlaylist()
|
||
|
|
||
|
// Create the media playlists for each output configuration.
|
||
|
for _, config := range configs {
|
||
|
// Verify the validity of the configuration.
|
||
|
if err := config.Validate(); err != nil {
|
||
|
return nil, errors.Wrap(err, "invalid output configuration")
|
||
|
}
|
||
|
|
||
|
mediaPlaylist, err := p.GenerateMediaPlaylistForClipAndConfiguration(clipId, config.ID)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to create clip media playlist")
|
||
|
}
|
||
|
|
||
|
// Append the media playlist to the master playlist.
|
||
|
params := p.getMediaPlaylistParamsForConfig(config)
|
||
|
|
||
|
// Add the media playlist to the master playlist.
|
||
|
publicPlaylistPath := strings.Join([]string{"/clip", clipId, config.ID}, "/")
|
||
|
masterPlaylist.Append(publicPlaylistPath, mediaPlaylist, params)
|
||
|
}
|
||
|
|
||
|
// Return the final master playlist that contains all the media playlists.
|
||
|
return masterPlaylist, nil
|
||
|
}
|
||
|
|
||
|
// GenerateMediaPlaylistForClipAndConfiguration returns a media playlist for a
|
||
|
// given clip Id and output configuration.
|
||
|
func (p *PlaylistGenerator) GenerateMediaPlaylistForClipAndConfiguration(clipId, outputConfigurationId string) (*m3u8.MediaPlaylist, error) {
|
||
|
clip, err := p.GetClip(clipId)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to get stream")
|
||
|
}
|
||
|
|
||
|
config, err := p.GetOutputConfig(outputConfigurationId)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to get output configuration")
|
||
|
}
|
||
|
|
||
|
clipStartSeconds := clip.RelativeStartTime
|
||
|
clipEndSeconds := clip.RelativeEndTime
|
||
|
|
||
|
// Fetch all the segments for this configuration.
|
||
|
segments, err := p.GetAllSegmentsForOutputConfigurationAndWindow(outputConfigurationId, clipStartSeconds, clipEndSeconds)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to get all clip segments for output configuration")
|
||
|
}
|
||
|
|
||
|
// Create the media playlist for this configuration and add the segments.
|
||
|
mediaPlaylist, err := p.createMediaPlaylistForConfigurationAndSegments(config, clip.Timestamp, false, segments)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to create clip media playlist")
|
||
|
}
|
||
|
|
||
|
return mediaPlaylist, nil
|
||
|
}
|
||
|
|
||
|
// GetClip returns a clip by its ID.
|
||
|
func (p *PlaylistGenerator) GetClip(clipId string) (*Clip, error) {
|
||
|
clip, err := p.datastore.GetQueries().GetClip(context.Background(), clipId)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to get clip")
|
||
|
}
|
||
|
|
||
|
if clip.ClipID == "" {
|
||
|
return nil, errors.Wrap(err, "failed to get clip")
|
||
|
}
|
||
|
|
||
|
if !clip.RelativeEndTime.Valid {
|
||
|
return nil, errors.Wrap(err, "failed to get clip")
|
||
|
}
|
||
|
|
||
|
timestamp, err := models.FlexibleDateParse(clip.ClipTimestamp)
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to parse clip timestamp")
|
||
|
}
|
||
|
|
||
|
c := Clip{
|
||
|
ID: clip.ClipID,
|
||
|
StreamId: clip.StreamID,
|
||
|
ClipTitle: clip.ClipTitle.String,
|
||
|
RelativeStartTime: float32(clip.RelativeStartTime.Float64),
|
||
|
RelativeEndTime: float32(clip.RelativeEndTime.Float64),
|
||
|
Timestamp: timestamp,
|
||
|
}
|
||
|
|
||
|
return &c, nil
|
||
|
}
|
||
|
|
||
|
// GetAllSegmentsForOutputConfigurationAndWindow returns all the segments for a
|
||
|
// given output config and time window.
|
||
|
func (p *PlaylistGenerator) GetAllSegmentsForOutputConfigurationAndWindow(configId string, startSeconds, endSeconds float32) ([]HLSSegment, error) {
|
||
|
segmentRows, err := p.datastore.GetQueries().GetSegmentsForOutputIdAndWindow(context.Background(), db.GetSegmentsForOutputIdAndWindowParams{
|
||
|
OutputConfigurationID: configId,
|
||
|
StartSeconds: startSeconds,
|
||
|
EndSeconds: endSeconds,
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, errors.Wrap(err, "failed to get clip segments for output config")
|
||
|
}
|
||
|
|
||
|
segments := []HLSSegment{}
|
||
|
for _, row := range segmentRows {
|
||
|
segment := HLSSegment{
|
||
|
ID: row.ID,
|
||
|
StreamID: row.StreamID,
|
||
|
OutputConfigurationID: row.OutputConfigurationID,
|
||
|
Timestamp: row.Timestamp.Time,
|
||
|
Path: row.Path,
|
||
|
}
|
||
|
segments = append(segments, segment)
|
||
|
}
|
||
|
|
||
|
return segments, nil
|
||
|
}
|