create on the fly hls livestream when video is started and stream it to client

This commit is contained in:
2022-12-25 23:19:04 +01:00
parent af1de3a244
commit 5b8a63c0aa
7 changed files with 239 additions and 9 deletions

View File

@ -3,12 +3,15 @@ package api
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"openmediacenter/apiGo/api/api"
"openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/config"
"openmediacenter/apiGo/database"
"openmediacenter/apiGo/videoparser/hls"
"os"
"os/exec"
"strconv"
"strings"
)
@ -398,6 +401,62 @@ func loadVideosHandlers() {
context.Json(result)
})
api.AddHandler("loadM3U8", api.VideoNode, api.PermUnauthorized, func(ctx api.Context) {
param := ctx.GetRequest().URL.Query().Get("id")
id, _ := strconv.Atoi(param)
mylist :=
`#EXTM3U
#EXT-X-VERSION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:NO
#EXT-X-TARGETDURATION:10
#EXT-X-START:TIME-OFFSET=0
#EXT-X-PLAYLIST-TYPE:VOD
`
// ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input.mp4
cmd := exec.Command("ffprobe",
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
hls.GetVideoPathById(uint32(id)))
stdout, err := cmd.Output()
//
if err != nil {
fmt.Println(err.Error())
fmt.Println(string(err.(*exec.ExitError).Stderr))
}
secss, _, _ := strings.Cut(string(stdout), ".")
secsi, err := strconv.Atoi(secss)
i := 0
for ; i < secsi/10; i++ {
mylist += fmt.Sprintf(
`#EXTINF:10.0,
/api/video/getVideoSegment?id=%d&idx=%d
`, id, i)
}
mylist += fmt.Sprintf(
`#EXTINF:%s,
/api/video/getVideoSegment?id=%d&idx=%d
EXT-X-ENDLIST
`, fmt.Sprintf("%d.0", secsi%10), id, i)
ctx.Text(mylist)
})
api.AddHandler("getVideoSegment", api.VideoNode, api.PermUnauthorized, func(ctx api.Context) {
params := ctx.GetRequest().URL.Query()
idxs := params.Get("idx")
ids := params.Get("id")
id, _ := strconv.Atoi(ids)
idx, _ := strconv.Atoi(idxs)
// todo error handling
tmppath := hls.GetSegment(uint32(idx), uint32(id))
http.ServeFile(ctx.GetWriter(), ctx.GetRequest(), tmppath)
})
}
func addToVideoHandlers() {

View File

@ -24,6 +24,7 @@ type FeaturesT struct {
type GeneralT struct {
VerboseLogging bool
ReindexPrefix string
TmpDir string
}
type FileConfT struct {
@ -44,6 +45,7 @@ func defaultConfig() *FileConfT {
General: GeneralT{
VerboseLogging: false,
ReindexPrefix: "/var/www/openmediacenter",
TmpDir: "/tmp/openmediacenter",
},
Features: FeaturesT{
DisableTVSupport: false,

View File

@ -0,0 +1,133 @@
package hls
import (
"fmt"
"log"
"openmediacenter/apiGo/config"
"openmediacenter/apiGo/database"
"os"
"os/exec"
"time"
)
type TranscodingState struct {
Finished bool
Active bool
}
var transcodeAcive = make(map[uint32]TranscodingState)
func startSegmentation(videoid uint32) {
transcodeAcive[videoid] = TranscodingState{
Finished: false,
Active: true,
}
cfg := config.GetConfig()
outpath := fmt.Sprintf("%s/%d", cfg.General.TmpDir, videoid)
if !fileExists(outpath) {
err := os.MkdirAll(outpath, os.ModePerm)
if err != nil {
log.Println(err)
}
}
app := "ffmpeg"
inputpath := GetVideoPathById(videoid)
fmt.Println(inputpath)
cmd := exec.Command(app,
//"-hide_banner",
//"-loglevel", "panic",
"-n",
//"-t", "10.0",
//"-ss", fmt.Sprintf("%d", id*10),
"-i", inputpath,
//"-g", "52",
//"-sc_threshold", "0",
//"-force_key_frames", "expr:gte(t,n_forced*10)",
//"-strict", "experimental",
//"-movflags", "+frag_keyframe+separate_moof+omit_tfhd_offset+empty_moov",
//"-c:v", "libx264",
//"-crf", "18",
//
//"-f", "segment",
//"-segment_time_delta", "0.2",
//"-segment_format", "mpegts",
//"-segment_times", commaSeparatedTimes,
//"-segment_start_number", `0`,
//"-segment_list_type", "flat",
//"-segment_list",
"-preset", "veryfast",
//"-maxrate", "4000k",
//"-bufsize", "8000k",
//"-vf", "scale=1280:-1,format=yuv420p",
//"-c:a", "aac",
"-force_key_frames", "expr:gte(t,n_forced*10)",
"-strict", "-2",
"-c:a", "aac",
"-c:v", "libx264",
"-f", "segment",
"-segment_list_type", "m3u8",
"-segment_time", "10.0",
"-segment_time_delta", "0.001",
"-segment_list", "test.m3u8", outpath+"/part%02d.ts")
stdout, err := cmd.Output()
//
if err != nil {
fmt.Println(err.Error())
fmt.Println(string(err.(*exec.ExitError).Stderr))
}
fmt.Println(stdout)
fmt.Println("finished transcoding")
transcodeAcive[videoid] = TranscodingState{
Finished: true,
Active: false,
}
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
func GetVideoPathById(videoid uint32) string {
query := fmt.Sprintf(`SELECT movie_url FROM videos WHERE movie_id=%d`, videoid)
var url string
err := database.QueryRow(query).Scan(&url)
if err != nil {
return ""
}
mSettings, _, _ := database.GetSettings()
vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath
return vidFolder + url
}
func GetSegment(segIdx uint32, videoid uint32) string {
cfg := config.GetConfig()
i, ok := transcodeAcive[videoid]
if ok {
if !i.Active && !i.Finished {
go startSegmentation(videoid)
}
} else {
go startSegmentation(videoid)
}
// todo timeout
tspath := fmt.Sprintf("%s/%d/part%02d.ts", cfg.General.TmpDir, videoid, segIdx)
if ok && i.Finished == true {
return tspath
}
fmt.Println("checking if part exists")
for !fileExists(fmt.Sprintf("%s/%d/part%02d.ts", cfg.General.TmpDir, videoid, segIdx+1)) {
time.Sleep(100 * time.Millisecond)
}
return tspath
}