Fixed offline video state to use fMP4 segments instead of launching ffmpeg with the old TS file.

This commit is contained in:
2026-04-02 09:11:00 -04:00
parent 9c290fa0d7
commit f0aa545d32
7 changed files with 72 additions and 61 deletions
+35 -10
View File
@@ -1,6 +1,7 @@
package core
import (
"fmt"
"os"
"path"
"path/filepath"
@@ -98,23 +99,47 @@ func createInitialOfflineState() error {
// offline video stream state only. No live stream HLS segments will continue to be
// referenced.
func transitionToOfflineVideoStreamContent() {
log.Traceln("Firing transcoder with offline stream state")
log.Traceln("Placing offline fMP4 content into HLS directories")
_transcoder := transcoder.NewTranscoder()
_transcoder.SetIdentifier("offline")
_transcoder.SetLatencyLevel(models.GetLatencyLevel(4))
_transcoder.SetIsEvent(true)
configRepository := configrepository.Get()
offlineFilePath, err := saveOfflineClipToDisk("offline-v2.ts")
offlineInitPath, offlineSegmentPath, err := saveOfflineFMP4ToDisk()
if err != nil {
log.Fatalln("unable to save offline clip:", err)
log.Fatalln("unable to save offline fMP4 files:", err)
}
_transcoder.SetInput(offlineFilePath)
go _transcoder.Start(false)
variants := configRepository.GetStreamOutputVariants()
if len(variants) == 0 {
// Ensure at least one variant directory exists.
variants = make([]models.StreamOutputVariant, 1)
}
for index := range variants {
variantDir := filepath.Join(config.HLSStoragePath, fmt.Sprintf("%d", index))
if err := os.MkdirAll(variantDir, 0o750); err != nil {
log.Errorln("unable to create variant directory:", err)
continue
}
makeVariantIndexOffline(index, offlineInitPath, offlineSegmentPath)
}
// Write the master playlist that points to each variant's playlist.
masterPlaylistPath := filepath.Join(config.HLSStoragePath, "stream.m3u8")
masterFile, err := os.Create(masterPlaylistPath) //nolint:gosec
if err != nil {
log.Errorln("unable to create master playlist:", err)
} else {
defer masterFile.Close()
_, _ = masterFile.WriteString("#EXTM3U\n")
_, _ = masterFile.WriteString("#EXT-X-VERSION:7\n")
_, _ = masterFile.WriteString("#EXT-X-INDEPENDENT-SEGMENTS\n")
for index := range variants {
_, _ = fmt.Fprintf(masterFile, "#EXT-X-STREAM-INF:BANDWIDTH=0\n")
_, _ = fmt.Fprintf(masterFile, "%d/stream.m3u8\n", index)
}
}
// Copy the logo to be the thumbnail
configRepository := configrepository.Get()
logo := configRepository.GetLogoPath()
dst := filepath.Join(config.TempDir, "thumbnail.jpg")
if err = utils.Copy(filepath.Join("data", logo), dst); err != nil {
+34 -40
View File
@@ -5,14 +5,13 @@ import (
"os"
"path/filepath"
"github.com/grafov/m3u8"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/static"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
func appendOfflineToVariantPlaylist(index int, playlistFilePath string) {
func appendOfflineToVariantPlaylist(index int, playlistFilePath string, initFilename string, segmentFilename string) {
existingPlaylistContents, err := os.ReadFile(playlistFilePath) // nolint: gosec
if err != nil {
log.Debugln("unable to read existing playlist file", err)
@@ -34,9 +33,10 @@ func appendOfflineToVariantPlaylist(index int, playlistFilePath string) {
// Manually append the offline clip to the end of the media playlist.
_, _ = atomicWriteTmpPlaylistFile.WriteString("#EXT-X-DISCONTINUITY\n")
_, _ = fmt.Fprintf(atomicWriteTmpPlaylistFile, "#EXT-X-MAP:URI=\"%s\"\n", initFilename)
// If "offline" content gets changed then change the duration below
_, _ = atomicWriteTmpPlaylistFile.WriteString("#EXTINF:8.000000,\n")
_, _ = atomicWriteTmpPlaylistFile.WriteString("offline-v2.ts\n")
_, _ = fmt.Fprintf(atomicWriteTmpPlaylistFile, "%s\n", segmentFilename)
_, _ = atomicWriteTmpPlaylistFile.WriteString("#EXT-X-ENDLIST\n")
if err := atomicWriteTmpPlaylistFile.Close(); err != nil {
@@ -48,48 +48,57 @@ func appendOfflineToVariantPlaylist(index int, playlistFilePath string) {
}
}
func makeVariantIndexOffline(index int, offlineFilePath string, offlineFilename string) {
playlistFilePath := fmt.Sprintf(filepath.Join(config.HLSStoragePath, "%d/stream.m3u8"), index)
segmentFilePath := fmt.Sprintf(filepath.Join(config.HLSStoragePath, "%d/%s"), index, offlineFilename)
func makeVariantIndexOffline(index int, offlineInitPath string, offlineSegmentPath string) {
variantDir := filepath.Join(config.HLSStoragePath, fmt.Sprintf("%d", index))
playlistFilePath := filepath.Join(variantDir, "stream.m3u8")
if err := utils.Copy(offlineFilePath, segmentFilePath); err != nil {
initDest := filepath.Join(variantDir, "offline-init.mp4")
segmentDest := filepath.Join(variantDir, "offline-v2.m4s")
if err := utils.Copy(offlineInitPath, initDest); err != nil {
log.Warnln(err)
return
}
if _, err := _storage.Save(initDest, 0); err != nil {
log.Warnln(err)
}
if _, err := _storage.Save(segmentFilePath, 0); err != nil {
if err := utils.Copy(offlineSegmentPath, segmentDest); err != nil {
log.Warnln(err)
return
}
if _, err := _storage.Save(segmentDest, 0); err != nil {
log.Warnln(err)
}
if utils.DoesFileExists(playlistFilePath) {
appendOfflineToVariantPlaylist(index, playlistFilePath)
appendOfflineToVariantPlaylist(index, playlistFilePath, "offline-init.mp4", "offline-v2.m4s")
} else {
createEmptyOfflinePlaylist(playlistFilePath, offlineFilename)
createEmptyOfflinePlaylist(playlistFilePath, "offline-init.mp4", "offline-v2.m4s")
}
if _, err := _storage.Save(playlistFilePath, 0); err != nil {
log.Warnln(err)
}
}
func createEmptyOfflinePlaylist(playlistFilePath string, offlineFilename string) {
p, err := m3u8.NewMediaPlaylist(1, 1)
if err != nil {
log.Errorln(err)
}
// If "offline" content gets changed then change the duration below
if err := p.Append(offlineFilename, 8.0, ""); err != nil {
log.Errorln(err)
}
p.Close()
func createEmptyOfflinePlaylist(playlistFilePath string, initFilename string, segmentFilename string) {
f, err := os.Create(playlistFilePath) //nolint:gosec
if err != nil {
log.Errorln(err)
return
}
defer f.Close()
if _, err := f.Write(p.Encode().Bytes()); err != nil {
log.Errorln(err)
}
_, _ = f.WriteString("#EXTM3U\n")
_, _ = f.WriteString("#EXT-X-VERSION:7\n")
_, _ = f.WriteString("#EXT-X-TARGETDURATION:8\n")
_, _ = fmt.Fprintf(f, "#EXT-X-MAP:URI=\"%s\"\n", initFilename)
// If "offline" content gets changed then change the duration below
_, _ = f.WriteString("#EXTINF:8.000000,\n")
_, _ = fmt.Fprintf(f, "%s\n", segmentFilename)
_, _ = f.WriteString("#EXT-X-ENDLIST\n")
}
func saveOfflineFMP4ToDisk() (initPath string, segmentPath string, err error) {
@@ -121,18 +130,3 @@ func saveOfflineFMP4ToDisk() (initPath string, segmentPath string, err error) {
return initPath, segmentPath, nil
}
func saveOfflineClipToDisk(offlineFilename string) (string, error) {
offlineFileData := static.GetOfflineSegment()
offlineTmpFile, err := os.CreateTemp(config.TempDir, offlineFilename)
if err != nil {
log.Errorln("unable to create temp file for offline video segment", err)
}
if _, err = offlineTmpFile.Write(offlineFileData); err != nil {
return "", fmt.Errorf("unable to write offline segment to disk: %s", err)
}
offlineFilePath := offlineTmpFile.Name()
return filepath.Abs(offlineFilePath)
}
+1 -1
View File
@@ -82,7 +82,7 @@ func getAllFilesRecursive(baseDirectory string) (map[string][]os.FileInfo, error
directory = info.Name()
}
if filepath.Ext(info.Name()) == ".m4s" {
if filepath.Ext(info.Name()) == ".m4s" || filepath.Ext(info.Name()) == ".mp4" {
files[directory] = append(files[directory], info)
}
+1 -1
View File
@@ -317,7 +317,7 @@ func (s *S3Storage) retrieveAllVideoSegments() ([]s3object, error) {
err := s.s3Client.ListObjectsPages(allObjectsListRequest,
func(page *s3.ListObjectsOutput, lastPage bool) bool {
for _, item := range page.Contents {
if strings.HasSuffix(*item.Key, ".m4s") {
if strings.HasSuffix(*item.Key, ".m4s") || strings.HasSuffix(*item.Key, ".mp4") {
allObjects = append(allObjects, s3object{
key: *item.Key,
lastModified: *item.LastModified,
BIN
View File
Binary file not shown.
-8
View File
@@ -51,14 +51,6 @@ func GetWebIndexTemplate() (*template.Template, error) {
return tmpl, err
}
//go:embed offline-v2.ts
var offlineVideoSegment []byte
// GetOfflineSegment will return the offline video segment data.
func GetOfflineSegment() []byte {
return getFileSystemStaticFileOrDefault("offline-v2.ts", offlineVideoSegment)
}
//go:embed offline-init.mp4
var offlineInitSegment []byte
+1 -1
View File
@@ -221,7 +221,7 @@ func GetCacheDurationSecondsForPath(filePath string) int {
} else if fileExtension == ".js" || fileExtension == ".css" {
// Cache javascript & CSS
return 60 * 60 * 24 * defaultDaysCached
} else if fileExtension == ".ts" || fileExtension == ".woff2" {
} else if fileExtension == ".m4s" || fileExtension == ".mp4" || fileExtension == ".woff2" {
// Cache video segments as long as you want. They can't change.
// This matters most for local hosting of segments for recordings
// and not for live or 3rd party storage.