livestream/hlsgenerator/main.go

137 lines
2.8 KiB
Go

package main
import (
"fmt"
"log"
"os"
"os/exec"
"strconv"
"strings"
"time"
)
var streamFileName = "stream.m3u8"
var segmentsFolder = "segments"
var maxSegments = 10
type State struct {
UpdateCount int
Segments []Segment
LiveSegments []Segment
}
func (s *State) ParseSegments() {
entries, err := os.ReadDir(segmentsFolder)
if err != nil {
log.Fatalln("Failed to open segments directory: ", err)
return
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".ts") {
continue
}
name := fmt.Sprintf("%s/%s", segmentsFolder, entry.Name())
duration, err := FfprobeGetDuration(name)
if err != nil {
log.Fatal("Failed to parse duration: ", err)
}
fmt.Println("Found segment:", name, "d:", duration)
s.Segments = append(s.Segments, Segment{
Length: duration,
FileName: name,
})
}
s.LiveSegments = s.Segments
}
// Example program for generating a livestream of a loading spinner using precut hls segments
func main() {
state := State{}
state.ParseSegments()
firstPass := true
for {
for i, segment := range state.LiveSegments {
if !firstPass {
// Adds current segment to end of playlist
state.LiveSegments = append(state.LiveSegments, state.Segments[i%len(state.Segments)])
}
fmt.Println("Adding segment", segment.FileName)
if firstPass {
UpdateFile(&state.UpdateCount, state.LiveSegments[:i+1])
} else {
// Limits playlist to 10 segments
if len(state.LiveSegments) > maxSegments {
state.LiveSegments = state.LiveSegments[i+1:]
}
UpdateFile(&state.UpdateCount, state.LiveSegments)
}
d, err := segment.Duration()
if err != nil {
log.Fatal(err)
}
fmt.Println("Sleeping for", d)
time.Sleep(d)
}
if firstPass {
fmt.Println("First pass over")
firstPass = false
}
fmt.Println("Starting loop over")
}
}
type Segment struct {
Length float64
FileName string
}
func (s *Segment) Duration() (time.Duration, error) {
d, err := time.ParseDuration(fmt.Sprintf("%fs", s.Length))
if err != nil {
return 0, err
}
return d, nil
}
func FfprobeGetDuration(filename string) (float64, error) {
cmd := exec.Command("ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filename)
output, err := cmd.Output()
if err != nil {
return -1, err
}
return strconv.ParseFloat(strings.TrimSpace(string(output)), 64)
}
func (s Segment) String() string {
return fmt.Sprintf("#EXTINF:%f\n%s\n", s.Length, s.FileName)
}
func UpdateFile(updateCount *int, segments []Segment) {
var segmentsFmt string
for _, segment := range segments {
segmentsFmt += segment.String()
}
stream := fmt.Sprintf(`#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:%d
%s`, *updateCount, segmentsFmt)
os.WriteFile(streamFileName, []byte(stream), 0644)
*updateCount++
}