137 lines
2.8 KiB
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++
|
|
}
|