Add prototyping programs
This commit is contained in:
parent
cce62f6f1a
commit
85f47cdbd8
11 changed files with 250 additions and 0 deletions
4
go.work
4
go.work
|
@ -3,3 +3,7 @@ go 1.23.4
|
||||||
use ./admincli
|
use ./admincli
|
||||||
|
|
||||||
use ./server
|
use ./server
|
||||||
|
|
||||||
|
use ./segmentwatcher
|
||||||
|
|
||||||
|
use ./hlsgenerator
|
||||||
|
|
7
go.work.sum
Normal file
7
go.work.sum
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
|
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
9
hlsgenerator/README.md
Normal file
9
hlsgenerator/README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# HLSGenerator
|
||||||
|
|
||||||
|
Simple test program that manages hls segments itself and lays them out in a m3u8 playlist
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p segments
|
||||||
|
ffmpeg -i example.webm -c:v libx264 -c:a aac -b:a 128k -ac 2 -pix_fmt yuv420p -preset ultrafas
|
||||||
|
t -hls_time 1 -f hls segments/index.m3u8
|
||||||
|
```
|
BIN
hlsgenerator/example.webm
Normal file
BIN
hlsgenerator/example.webm
Normal file
Binary file not shown.
3
hlsgenerator/go.mod
Normal file
3
hlsgenerator/go.mod
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module hlsgenerator
|
||||||
|
|
||||||
|
go 1.23.4
|
137
hlsgenerator/main.go
Normal file
137
hlsgenerator/main.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
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++
|
||||||
|
}
|
9
segmentwatcher/README.md
Normal file
9
segmentwatcher/README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# segmentwatcher
|
||||||
|
|
||||||
|
Example program
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir -p hls
|
||||||
|
ffmpeg -re -i test-video.mkv -hls_flags delete_segments+append_list+omit_endlist -f hls hls/
|
||||||
|
index.m3u8
|
||||||
|
```
|
7
segmentwatcher/go.mod
Normal file
7
segmentwatcher/go.mod
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
module segmentwatcher
|
||||||
|
|
||||||
|
go 1.23.4
|
||||||
|
|
||||||
|
require github.com/fsnotify/fsnotify v1.8.0
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.29.0 // indirect
|
4
segmentwatcher/go.sum
Normal file
4
segmentwatcher/go.sum
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
|
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
70
segmentwatcher/main.go
Normal file
70
segmentwatcher/main.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
var segmentList = []string{}
|
||||||
|
|
||||||
|
func remove(slice []string, s int) []string {
|
||||||
|
return append(slice[:s], slice[s+1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printSegmentList() {
|
||||||
|
log.Printf("Segment list: %s\n", strings.Join(segmentList, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
log.Println("Starting segment watcher")
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer watcher.Close()
|
||||||
|
|
||||||
|
// Start listening for events.
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(event.Name, ".ts") {
|
||||||
|
if event.Op == fsnotify.Create {
|
||||||
|
segmentList = append(segmentList, event.Name)
|
||||||
|
log.Println("Created file", event.Name)
|
||||||
|
printSegmentList()
|
||||||
|
} else if event.Op == fsnotify.Remove {
|
||||||
|
index := slices.Index(segmentList, event.Name)
|
||||||
|
if index != -1 {
|
||||||
|
segmentList = remove(segmentList, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("Removed file", event.Name)
|
||||||
|
printSegmentList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case err, ok := <-watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Add a path.
|
||||||
|
err = watcher.Add("hls")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block main goroutine forever.
|
||||||
|
<-make(chan struct{})
|
||||||
|
}
|
BIN
segmentwatcher/test-video.mkv
Normal file
BIN
segmentwatcher/test-video.mkv
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue