Skip to content

Commit

Permalink
Add support for live playlists in SegmentedFileOutput (#476)
Browse files Browse the repository at this point in the history
  • Loading branch information
biglittlebigben authored Sep 25, 2023
1 parent 1faec82 commit 64753fb
Show file tree
Hide file tree
Showing 15 changed files with 371 additions and 118 deletions.
58 changes: 31 additions & 27 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,62 +134,66 @@ func TestSegmentNaming(t *testing.T) {
})

for _, test := range []struct {
filenamePrefix string
playlistName string
expectedStorageDir string
expectedPlaylistFilename string
expectedSegmentPrefix string
filenamePrefix string
playlistName string
livePlaylistName string
expectedStorageDir string
expectedPlaylistFilename string
expectedLivePlaylistFilename string
expectedSegmentPrefix string
}{
{
filenamePrefix: "", playlistName: "playlist",
expectedStorageDir: "", expectedPlaylistFilename: "playlist.m3u8", expectedSegmentPrefix: "playlist",
filenamePrefix: "", playlistName: "playlist", livePlaylistName: "",
expectedStorageDir: "", expectedPlaylistFilename: "playlist.m3u8", expectedLivePlaylistFilename: "", expectedSegmentPrefix: "playlist",
},
{
filenamePrefix: "", playlistName: "conf_test/playlist",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "playlist.m3u8", expectedSegmentPrefix: "playlist",
filenamePrefix: "", playlistName: "conf_test/playlist", livePlaylistName: "conf_test/live_playlist",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "playlist.m3u8", expectedLivePlaylistFilename: "live_playlist.m3u8", expectedSegmentPrefix: "playlist",
},
{
filenamePrefix: "filename", playlistName: "",
expectedStorageDir: "", expectedPlaylistFilename: "filename.m3u8", expectedSegmentPrefix: "filename",
filenamePrefix: "filename", playlistName: "", livePlaylistName: "live_playlist2.m3u8",
expectedStorageDir: "", expectedPlaylistFilename: "filename.m3u8", expectedLivePlaylistFilename: "live_playlist2.m3u8", expectedSegmentPrefix: "filename",
},
{
filenamePrefix: "filename", playlistName: "playlist",
expectedStorageDir: "", expectedPlaylistFilename: "playlist.m3u8", expectedSegmentPrefix: "filename",
filenamePrefix: "filename", playlistName: "playlist", livePlaylistName: "",
expectedStorageDir: "", expectedPlaylistFilename: "playlist.m3u8", expectedLivePlaylistFilename: "", expectedSegmentPrefix: "filename",
},
{
filenamePrefix: "filename", playlistName: "conf_test/",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "filename.m3u8", expectedSegmentPrefix: "filename",
filenamePrefix: "filename", playlistName: "conf_test/", livePlaylistName: "",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "filename.m3u8", expectedLivePlaylistFilename: "", expectedSegmentPrefix: "filename",
},
{
filenamePrefix: "filename", playlistName: "conf_test/playlist",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "playlist.m3u8", expectedSegmentPrefix: "filename",
filenamePrefix: "filename", playlistName: "conf_test/playlist", livePlaylistName: "",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "playlist.m3u8", expectedLivePlaylistFilename: "", expectedSegmentPrefix: "filename",
},
{
filenamePrefix: "conf_test/", playlistName: "playlist",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "playlist.m3u8", expectedSegmentPrefix: "playlist",
filenamePrefix: "conf_test/", playlistName: "playlist", livePlaylistName: "",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "playlist.m3u8", expectedLivePlaylistFilename: "", expectedSegmentPrefix: "playlist",
},
{
filenamePrefix: "conf_test/filename", playlistName: "playlist",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "playlist.m3u8", expectedSegmentPrefix: "filename",
filenamePrefix: "conf_test/filename", playlistName: "playlist", livePlaylistName: "",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "playlist.m3u8", expectedLivePlaylistFilename: "", expectedSegmentPrefix: "filename",
},
{
filenamePrefix: "conf_test/filename", playlistName: "conf_test/playlist",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "playlist.m3u8", expectedSegmentPrefix: "filename",
filenamePrefix: "conf_test/filename", playlistName: "conf_test/playlist", livePlaylistName: "",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "playlist.m3u8", expectedLivePlaylistFilename: "", expectedSegmentPrefix: "filename",
},
{
filenamePrefix: "conf_test_2/filename", playlistName: "conf_test/playlist",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "playlist.m3u8", expectedSegmentPrefix: "conf_test_2/filename",
filenamePrefix: "conf_test_2/filename", playlistName: "conf_test/playlist", livePlaylistName: "",
expectedStorageDir: "conf_test/", expectedPlaylistFilename: "playlist.m3u8", expectedLivePlaylistFilename: "", expectedSegmentPrefix: "conf_test_2/filename",
},
} {
p := &PipelineConfig{Info: &livekit.EgressInfo{EgressId: "egress_ID"}}
o, err := p.getSegmentConfig(&livekit.SegmentedFileOutput{
FilenamePrefix: test.filenamePrefix,
PlaylistName: test.playlistName,
FilenamePrefix: test.filenamePrefix,
PlaylistName: test.playlistName,
LivePlaylistName: test.livePlaylistName,
})
require.NoError(t, err)

require.Equal(t, test.expectedStorageDir, o.StorageDir)
require.Equal(t, test.expectedPlaylistFilename, o.PlaylistFilename)
require.Equal(t, test.expectedLivePlaylistFilename, o.LivePlaylistFilename)
require.Equal(t, test.expectedSegmentPrefix, o.SegmentPrefix)
}
}
70 changes: 49 additions & 21 deletions pkg/config/output_segment.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,22 @@ import (
"strings"
"time"

"github.com/livekit/egress/pkg/errors"
"github.com/livekit/egress/pkg/types"
"github.com/livekit/protocol/livekit"
)

type SegmentConfig struct {
outputConfig

SegmentsInfo *livekit.SegmentsInfo
LocalDir string
StorageDir string
PlaylistFilename string
SegmentPrefix string
SegmentSuffix livekit.SegmentedFileSuffix
SegmentDuration int
SegmentsInfo *livekit.SegmentsInfo
LocalDir string
StorageDir string
PlaylistFilename string
LivePlaylistFilename string
SegmentPrefix string
SegmentSuffix livekit.SegmentedFileSuffix
SegmentDuration int

DisableManifest bool
UploadConfig UploadConfig
Expand All @@ -51,13 +53,14 @@ func (p *PipelineConfig) GetSegmentConfig() *SegmentConfig {
// segments should always be added last, so we can check keyframe interval from file/stream
func (p *PipelineConfig) getSegmentConfig(segments *livekit.SegmentedFileOutput) (*SegmentConfig, error) {
conf := &SegmentConfig{
SegmentsInfo: &livekit.SegmentsInfo{},
SegmentPrefix: clean(segments.FilenamePrefix),
SegmentSuffix: segments.FilenameSuffix,
PlaylistFilename: clean(segments.PlaylistName),
SegmentDuration: int(segments.SegmentDuration),
DisableManifest: segments.DisableManifest,
UploadConfig: p.getUploadConfig(segments),
SegmentsInfo: &livekit.SegmentsInfo{},
SegmentPrefix: clean(segments.FilenamePrefix),
SegmentSuffix: segments.FilenameSuffix,
PlaylistFilename: clean(segments.PlaylistName),
LivePlaylistFilename: clean(segments.LivePlaylistName),
SegmentDuration: int(segments.SegmentDuration),
DisableManifest: segments.DisableManifest,
UploadConfig: p.getUploadConfig(segments),
}

if conf.SegmentDuration == 0 {
Expand Down Expand Up @@ -91,26 +94,40 @@ func (p *PipelineConfig) getSegmentConfig(segments *livekit.SegmentedFileOutput)
return conf, nil
}

func removeKnownExtension(filename string) string {
if extIdx := strings.LastIndex(filename, "."); extIdx > -1 {
existingExt := types.FileExtension(filename[extIdx:])
if _, ok := types.FileExtensions[existingExt]; ok {
filename = filename[:extIdx]
}
filename = filename[:extIdx]
}

return filename
}

func (o *SegmentConfig) updatePrefixAndPlaylist(p *PipelineConfig) error {
identifier, replacements := p.getFilenameInfo()

o.SegmentPrefix = stringReplace(o.SegmentPrefix, replacements)
o.PlaylistFilename = stringReplace(o.PlaylistFilename, replacements)
o.LivePlaylistFilename = stringReplace(o.LivePlaylistFilename, replacements)

ext := types.FileExtensionForOutputType[o.OutputType]

playlistDir, playlistName := path.Split(o.PlaylistFilename)
livePlaylistDir, livePlaylistName := path.Split(o.LivePlaylistFilename)
fileDir, filePrefix := path.Split(o.SegmentPrefix)

// remove extension from playlist name
if extIdx := strings.LastIndex(playlistName, "."); extIdx > -1 {
existingExt := types.FileExtension(playlistName[extIdx:])
if _, ok := types.FileExtensions[existingExt]; ok {
playlistName = playlistName[:extIdx]
}
playlistName = playlistName[:extIdx]
// force live playlist to be in the same directory as the main playlist
if livePlaylistDir != "" && livePlaylistDir != playlistDir {
return errors.ErrInvalidInput("live_playlist_name must be in same directory as playlist_name")
}

// remove extension from playlist name
playlistName = removeKnownExtension(playlistName)
livePlaylistName = removeKnownExtension(livePlaylistName)

// only keep fileDir if it is a subdirectory of playlistDir
if fileDir != "" {
if playlistDir == fileDir {
Expand All @@ -130,6 +147,7 @@ func (o *SegmentConfig) updatePrefixAndPlaylist(p *PipelineConfig) error {
playlistName = fmt.Sprintf("%s-%s", identifier, time.Now().Format("2006-01-02T150405"))
}
}
// live playlist disabled by default

// ensure filePrefix
if filePrefix == "" {
Expand All @@ -139,8 +157,15 @@ func (o *SegmentConfig) updatePrefixAndPlaylist(p *PipelineConfig) error {
// update config
o.StorageDir = playlistDir
o.PlaylistFilename = fmt.Sprintf("%s%s", playlistName, ext)
if livePlaylistName != "" {
o.LivePlaylistFilename = fmt.Sprintf("%s%s", livePlaylistName, ext)
}
o.SegmentPrefix = fmt.Sprintf("%s%s", fileDir, filePrefix)

if o.PlaylistFilename == o.LivePlaylistFilename {
return errors.ErrInvalidInput("live_playlist_name cannot be identical to playlist_name")
}

if o.UploadConfig == nil {
o.LocalDir = playlistDir
} else {
Expand All @@ -162,5 +187,8 @@ func (o *SegmentConfig) updatePrefixAndPlaylist(p *PipelineConfig) error {
}

o.SegmentsInfo.PlaylistName = path.Join(o.StorageDir, o.PlaylistFilename)
if o.LivePlaylistFilename != "" {
o.SegmentsInfo.LivePlaylistName = path.Join(o.StorageDir, o.LivePlaylistFilename)
}
return nil
}
2 changes: 2 additions & 0 deletions pkg/config/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,8 +572,10 @@ func (p *PipelineConfig) UpdateInfoFromSDK(identifier string, replacements map[s
o.LocalDir = stringReplace(o.LocalDir, replacements)
o.StorageDir = stringReplace(o.StorageDir, replacements)
o.PlaylistFilename = stringReplace(o.PlaylistFilename, replacements)
o.LivePlaylistFilename = stringReplace(o.LivePlaylistFilename, replacements)
o.SegmentPrefix = stringReplace(o.SegmentPrefix, replacements)
o.SegmentsInfo.PlaylistName = stringReplace(o.SegmentsInfo.PlaylistName, replacements)
o.SegmentsInfo.LivePlaylistName = stringReplace(o.SegmentsInfo.LivePlaylistName, replacements)
}
}

Expand Down
Loading

0 comments on commit 64753fb

Please sign in to comment.