Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for live playlists in SegmentedFileOutput #476

Merged
merged 14 commits into from
Sep 25, 2023
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