mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-12-06 06:36:25 +00:00
Compare commits
11 Commits
f1077608fe
...
feat/fmp4
| Author | SHA1 | Date | |
|---|---|---|---|
| f03464aa4c | |||
| 2ae26d108d | |||
| a99317cce4 | |||
| 9fad5da0a4 | |||
| f6dab80a98 | |||
| 0579afe02b | |||
| 4fd25ce5ac | |||
| 59264bd42f | |||
| e85d2d4416 | |||
| 61f800d4c2 | |||
| 2808da8f6f |
@@ -238,6 +238,7 @@ export const Video = memo(function Video({
|
||||
showNotificationControls
|
||||
playInBackground
|
||||
playWhenInactive
|
||||
disableDisconnectError
|
||||
paused={!isPlaying}
|
||||
muted={isMuted}
|
||||
volume={volume}
|
||||
|
||||
@@ -32,6 +32,7 @@ in
|
||||
go
|
||||
wgo
|
||||
mediainfo
|
||||
bento4
|
||||
ffmpeg-full
|
||||
postgresql_15
|
||||
pgformatter
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/zoriya/kyoo/transcoder
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
require github.com/labstack/echo/v4 v4.12.0 // direct
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ func (h *Handler) GetAudioIndex(c echo.Context) error {
|
||||
//
|
||||
// Retrieve a chunk of a transmuxed video.
|
||||
//
|
||||
// Path: /:path/:quality/segments-:chunk.ts
|
||||
// Path: /:path/:quality/segments-:chunk.m4s
|
||||
func (h *Handler) GetVideoSegment(c echo.Context) error {
|
||||
quality, err := src.QualityFromString(c.Param("quality"))
|
||||
if err != nil {
|
||||
@@ -139,7 +139,7 @@ func (h *Handler) GetVideoSegment(c echo.Context) error {
|
||||
//
|
||||
// Retrieve a chunk of a transcoded audio.
|
||||
//
|
||||
// Path: /:path/audio/:audio/segments-:chunk.ts
|
||||
// Path: /:path/audio/:audio/segments-:chunk.m4s
|
||||
func (h *Handler) GetAudioSegment(c echo.Context) error {
|
||||
audio, err := strconv.ParseInt(c.Param("audio"), 10, 32)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,8 +18,8 @@ func NewAudioStream(file *FileStream, idx int32) *AudioStream {
|
||||
return ret
|
||||
}
|
||||
|
||||
func (as *AudioStream) getOutPath(encoder_id int) string {
|
||||
return fmt.Sprintf("%s/segment-a%d-%d-%%d.ts", as.file.Out, as.index, encoder_id)
|
||||
func (as *AudioStream) getIdentifier() string {
|
||||
return fmt.Sprintf("a%d", as.index)
|
||||
}
|
||||
|
||||
func (as *AudioStream) getFlags() Flags {
|
||||
@@ -36,3 +36,29 @@ func (as *AudioStream) getTranscodeArgs(segments string) []string {
|
||||
"-b:a", "128k",
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *AudioStream) GetIndex() (string, error) {
|
||||
index := `#EXTM3U
|
||||
#EXT-X-VERSION:7
|
||||
#EXT-X-PLAYLIST-TYPE:EVENT
|
||||
#EXT-X-START:TIME-OFFSET=0
|
||||
#EXT-X-MEDIA-SEQUENCE:0
|
||||
#EXT-X-INDEPENDENT-SEGMENTS
|
||||
#EXT-X-MAP:URI="init.mp4"
|
||||
`
|
||||
index += fmt.Sprintf("#EXT-X-TARGETDURATION:%d\n", int(OptimalFragmentDuration)+1)
|
||||
|
||||
count := int32((float64(ts.file.Info.Duration) / OptimalFragmentDuration))
|
||||
for segment := int32(0); segment < count; segment++ {
|
||||
index += fmt.Sprintf("#EXTINF:%.6f\n", OptimalFragmentDuration)
|
||||
index += fmt.Sprintf("segment-%d.m4s\n", segment)
|
||||
}
|
||||
|
||||
last_ts := float64(count) * OptimalFragmentDuration
|
||||
if last_ts > 0 {
|
||||
index += fmt.Sprintf("#EXTINF:%.6f\n", float64(ts.file.Info.Duration)-last_ts)
|
||||
index += fmt.Sprintf("segment-%d.m4s\n", count)
|
||||
}
|
||||
index += `#EXT-X-ENDLIST`
|
||||
return index, nil
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func Extract(path string, sha string) (<-chan struct{}, error) {
|
||||
}
|
||||
attachment_path := fmt.Sprintf("%s/%s/att", Settings.Metadata, sha)
|
||||
subs_path := fmt.Sprintf("%s/%s/sub", Settings.Metadata, sha)
|
||||
os.MkdirAll(attachment_path, 0o644)
|
||||
os.MkdirAll(attachment_path, 0o755)
|
||||
os.MkdirAll(subs_path, 0o755)
|
||||
|
||||
// If there is no subtitles, there is nothing to extract (also fonts would be useless).
|
||||
|
||||
@@ -68,7 +68,10 @@ func (fs *FileStream) Destroy() {
|
||||
}
|
||||
|
||||
func (fs *FileStream) GetMaster() string {
|
||||
master := "#EXTM3U\n"
|
||||
master := `#EXTM3U
|
||||
#EXT-X-VERSION:7
|
||||
#EXT-X-INDEPENDENT-SEGMENTS
|
||||
`
|
||||
if fs.Info.Video != nil {
|
||||
var transmux_quality Quality
|
||||
for _, quality := range Qualities {
|
||||
@@ -89,7 +92,7 @@ func (fs *FileStream) GetMaster() string {
|
||||
}
|
||||
master += "AUDIO=\"audio\","
|
||||
master += "CLOSED-CAPTIONS=NONE\n"
|
||||
master += fmt.Sprintf("./%s/index.m3u8\n", Original)
|
||||
master += fmt.Sprintf("%s/index.m3u8\n", Original)
|
||||
}
|
||||
|
||||
aspectRatio := float32(fs.Info.Video.Width) / float32(fs.Info.Video.Height)
|
||||
@@ -110,7 +113,7 @@ func (fs *FileStream) GetMaster() string {
|
||||
master += fmt.Sprintf("CODECS=\"%s\",", transmux_codec)
|
||||
master += "AUDIO=\"audio\","
|
||||
master += "CLOSED-CAPTIONS=NONE\n"
|
||||
master += fmt.Sprintf("./%s/index.m3u8\n", quality)
|
||||
master += fmt.Sprintf("%s/index.m3u8\n", quality)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,7 +133,7 @@ func (fs *FileStream) GetMaster() string {
|
||||
if audio.IsDefault {
|
||||
master += "DEFAULT=YES,"
|
||||
}
|
||||
master += fmt.Sprintf("URI=\"./audio/%d/index.m3u8\"\n", audio.Index)
|
||||
master += fmt.Sprintf("URI=\"audio/%d/index.m3u8\"\n", audio.Index)
|
||||
}
|
||||
return master
|
||||
}
|
||||
|
||||
@@ -4,16 +4,19 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// In seconds, the spec recomands 6 but since we don't control keyframes we go over more often than not.
|
||||
const OptimalFragmentDuration = float64(5)
|
||||
|
||||
type Keyframe struct {
|
||||
Sha string
|
||||
Keyframes []float64
|
||||
CanTransmux bool
|
||||
IsDone bool
|
||||
info *KeyframeInfo
|
||||
}
|
||||
@@ -35,7 +38,12 @@ func (kf *Keyframe) Slice(start int32, end int32) []float64 {
|
||||
}
|
||||
kf.info.mutex.RLock()
|
||||
defer kf.info.mutex.RUnlock()
|
||||
|
||||
ref := kf.Keyframes[start:end]
|
||||
if kf.IsDone {
|
||||
return ref
|
||||
}
|
||||
// make a copy since we will continue to mutate the array.
|
||||
ret := make([]float64, end-start)
|
||||
copy(ret, ref)
|
||||
return ret
|
||||
@@ -92,7 +100,7 @@ func GetKeyframes(sha string, path string) *Keyframe {
|
||||
}
|
||||
|
||||
func getKeyframes(path string, kf *Keyframe, sha string) error {
|
||||
defer printExecTime("ffprobe analysis for %s", path)()
|
||||
defer printExecTime("keyframe extraction for %s", path)()
|
||||
// run ffprobe to return all IFrames, IFrames are points where we can split the video in segments.
|
||||
// We ask ffprobe to return the time of each frame and it's flags
|
||||
// We could ask it to return only i-frames (keyframes) with the -skip_frame nokey but using it is extremly slow
|
||||
@@ -118,6 +126,7 @@ func getKeyframes(path string, kf *Keyframe, sha string) error {
|
||||
|
||||
ret := make([]float64, 0, 1000)
|
||||
max := 100
|
||||
last_frame := math.Inf(-1)
|
||||
done := 0
|
||||
// sometimes, videos can start at a timing greater than 0:00. We need to take that into account
|
||||
// and only list keyframes that come after the start of the video (without that, our segments count
|
||||
@@ -149,12 +158,14 @@ func getKeyframes(path string, kf *Keyframe, sha string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Before, we wanted to only save keyframes with at least 3s betweens
|
||||
// to prevent segments of 0.2s but sometimes, the -f segment muxer discards
|
||||
// the segment time and decide to cut at a random keyframe. Having every keyframe
|
||||
// handled as a segment prevents that.
|
||||
// The -f hls encoder decides to not cut at every keyframes (even if we ask it to) when they are too close by.
|
||||
// Instead we can cut every X seconds at the next keyframe, we'll use that as a marker.
|
||||
if fpts < (last_frame+OptimalFragmentDuration) {
|
||||
continue
|
||||
}
|
||||
|
||||
ret = append(ret, fpts)
|
||||
last_frame = fpts
|
||||
|
||||
if len(ret) == max {
|
||||
kf.add(ret)
|
||||
@@ -185,7 +196,7 @@ func getKeyframes(path string, kf *Keyframe, sha string) error {
|
||||
}
|
||||
|
||||
func getDummyKeyframes(path string, sha string) ([]float64, error) {
|
||||
dummyKeyframeDuration := float64(2)
|
||||
dummyKeyframeDuration := OptimalFragmentDuration
|
||||
info, err := GetInfo(path, sha)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -25,21 +24,30 @@ const (
|
||||
|
||||
type StreamHandle interface {
|
||||
getTranscodeArgs(segments string) []string
|
||||
getOutPath(encoder_id int) string
|
||||
getIdentifier() string
|
||||
getFlags() Flags
|
||||
GetIndex() (string, error)
|
||||
}
|
||||
|
||||
// First %d is encoder_id, second %d is segment number (escaped for ffmpeg)
|
||||
const (
|
||||
SegmentNameFormat = "%d-segment-%%d.m4s"
|
||||
InitNameFormat = "%d-init.mp4"
|
||||
)
|
||||
|
||||
type Stream struct {
|
||||
handle StreamHandle
|
||||
file *FileStream
|
||||
segments []Segment
|
||||
// An init.mp4 reference. Only one exists per stream
|
||||
init Segment
|
||||
heads []Head
|
||||
// the lock used for the the heads
|
||||
// the lock used for the heads
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
type Segment struct {
|
||||
// channel open if the segment is not ready. closed if ready.
|
||||
// channel open if the segment is not ready. Closed if ready.
|
||||
// one can check if segment 1 is open by doing:
|
||||
//
|
||||
// ts.isSegmentReady(1).
|
||||
@@ -67,12 +75,17 @@ func NewStream(file *FileStream, handle StreamHandle, ret *Stream) {
|
||||
ret.file = file
|
||||
ret.heads = make([]Head, 0)
|
||||
|
||||
ret.init.channel = make(chan struct{})
|
||||
|
||||
length, is_done := file.Keyframes.Length()
|
||||
ret.segments = make([]Segment, length, max(length, 2000))
|
||||
for seg := range ret.segments {
|
||||
ret.segments[seg].channel = make(chan struct{})
|
||||
}
|
||||
|
||||
// Try to encode asap, the client will first require the init.mp4 anyways so we can't know where to start.
|
||||
ret.run(0)
|
||||
|
||||
if !is_done {
|
||||
file.Keyframes.AddListener(func(keyframes []float64) {
|
||||
ret.lock.Lock()
|
||||
@@ -86,10 +99,25 @@ func NewStream(file *FileStream, handle StreamHandle, ret *Stream) {
|
||||
for seg := old_length; seg < len(keyframes); seg++ {
|
||||
ret.segments[seg].channel = make(chan struct{})
|
||||
}
|
||||
|
||||
// if we still haven't created the init.mp4 create it now.
|
||||
if len(ret.heads) == 0 {
|
||||
ret.run(0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (ts *Stream) isInitReady() bool {
|
||||
select {
|
||||
case <-ts.init.channel:
|
||||
// if the channel returned, it means it was closed
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Remember to lock before calling this.
|
||||
func (ts *Stream) isSegmentReady(segment int32) bool {
|
||||
select {
|
||||
@@ -119,8 +147,8 @@ func toSegmentStr(segments []float64) string {
|
||||
func (ts *Stream) run(start int32) error {
|
||||
// Start the transcode up to the 100th segment (or less)
|
||||
length, is_done := ts.file.Keyframes.Length()
|
||||
end := min(start+100, length)
|
||||
// if keyframes analysys is not finished, always have a 1-segment padding
|
||||
end := min(start+4, length)
|
||||
// if keyframes analysis is not finished, always have a 1-segment padding
|
||||
// for the extra segment needed for precise split (look comment before -to flag)
|
||||
if !is_done {
|
||||
end -= 2
|
||||
@@ -156,19 +184,19 @@ func (ts *Stream) run(start int32) error {
|
||||
|
||||
// Include both the start and end delimiter because -ss and -to are not accurate
|
||||
// Having an extra segment allows us to cut precisely the segments we want with the
|
||||
// -f segment that does cut the begining and the end at the keyframe like asked
|
||||
// -f segment that does cut the beginning and the end at the keyframe like asked
|
||||
start_ref := float64(0)
|
||||
start_segment := start
|
||||
if start != 0 {
|
||||
// we always take on segment before the current one, for different reasons for audio/video:
|
||||
// - Audio: we need context before the starting point, without that ffmpeg doesnt know what to do and leave ~100ms of silence
|
||||
// - Audio: we need context before the starting point, without that ffmpeg doesn't know what to do and leave ~100ms of silence
|
||||
// - Video: if a segment is really short (between 20 and 100ms), the padding given in the else block bellow is not enough and
|
||||
// the previous segment is played another time. the -segment_times is way more precise so it does not do the same with this one
|
||||
// the previous segment is played another time. The -segment_times is way more precise so it does not do the same with this one
|
||||
start_segment = start - 1
|
||||
if ts.handle.getFlags()&AudioF != 0 {
|
||||
start_ref = ts.file.Keyframes.Get(start_segment)
|
||||
} else {
|
||||
// the param for the -ss takes the keyframe before the specificed time
|
||||
// the param for the -ss takes the keyframe before the specified time
|
||||
// (if the specified time is a keyframe, it either takes that keyframe or the one before)
|
||||
// to prevent this weird behavior, we specify a bit after the keyframe that interest us
|
||||
|
||||
@@ -181,8 +209,8 @@ func (ts *Stream) run(start int32) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
end_padding := int32(1)
|
||||
if end == length {
|
||||
end_padding := int32(2)
|
||||
if end == length+1 {
|
||||
end_padding = 0
|
||||
}
|
||||
segments := ts.file.Keyframes.Slice(start_segment+1, end+end_padding)
|
||||
@@ -191,8 +219,8 @@ func (ts *Stream) run(start int32) error {
|
||||
segments = []float64{9999999}
|
||||
}
|
||||
|
||||
outpath := ts.handle.getOutPath(encoder_id)
|
||||
err := os.MkdirAll(filepath.Dir(outpath), 0o755)
|
||||
outpath := fmt.Sprintf("%s/%s", ts.file.Out, ts.handle.getIdentifier())
|
||||
err := os.MkdirAll(outpath, 0o755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -222,7 +250,7 @@ func (ts *Stream) run(start int32) error {
|
||||
end_ref := ts.file.Keyframes.Get(end + 1)
|
||||
// it seems that the -to is confused when -ss seek before the given time (because it searches for a keyframe)
|
||||
// add back the time that would be lost otherwise
|
||||
// this only appens when -to is before -i but having -to after -i gave a bug (not sure, don't remember)
|
||||
// this only happen when -to is before -i but having -to after -i gave a bug (not sure, don't remember)
|
||||
end_ref += start_ref - ts.file.Keyframes.Get(start_segment)
|
||||
args = append(args,
|
||||
"-to", fmt.Sprintf("%.6f", end_ref),
|
||||
@@ -243,24 +271,27 @@ func (ts *Stream) run(start int32) error {
|
||||
"-muxdelay", "0",
|
||||
)
|
||||
args = append(args, ts.handle.getTranscodeArgs(toSegmentStr(segments))...)
|
||||
|
||||
if start_ref != 0 {
|
||||
args = append(args,
|
||||
"-f", "segment",
|
||||
// needed for rounding issues when forcing keyframes
|
||||
// recommended value is 1/(2*frame_rate), which for a 24fps is ~0.021
|
||||
// we take a little bit more than that to be extra safe but too much can be harmfull
|
||||
// when segments are short (can make the video repeat itself)
|
||||
"-segment_time_delta", "0.05",
|
||||
"-segment_format", "mpegts",
|
||||
"-segment_times", toSegmentStr(Map(segments, func(seg float64, _ int) float64 {
|
||||
// segment_times want durations, not timestamps so we must substract the -ss param
|
||||
// since we give a greater value to -ss to prevent wrong seeks but -segment_times
|
||||
// needs precise segments, we use the keyframe we want to seek to as a reference.
|
||||
return seg - ts.file.Keyframes.Get(start_segment)
|
||||
})),
|
||||
"-segment_list_type", "flat",
|
||||
"-segment_list", "pipe:1",
|
||||
"-segment_start_number", fmt.Sprint(start_segment),
|
||||
outpath,
|
||||
// We need to add frag_discout to the movflags to get ffmpeg to calculate the correct presentation time otherwise players get lost.
|
||||
// Since we can't append to movflags but only override them, we also copy the default hls w/ fmp4 movflags
|
||||
// Initial flags from: https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/hlsenc.c#L934C32-L934C72
|
||||
"-hls_segment_options", "movflags=frag_custom+dash+delay_moov+frag_discont",
|
||||
)
|
||||
}
|
||||
args = append(args,
|
||||
"-f", "hls",
|
||||
"-hls_time", fmt.Sprint(OptimalFragmentDuration),
|
||||
"-start_number", fmt.Sprint(start_segment),
|
||||
"-hls_segment_type", "fmp4",
|
||||
"-hls_fmp4_init_filename", fmt.Sprintf("%s/%s", outpath, fmt.Sprintf(InitNameFormat, encoder_id)),
|
||||
"-hls_segment_filename", fmt.Sprintf("%s/%s", outpath, fmt.Sprintf(SegmentNameFormat, encoder_id)),
|
||||
// Make the playlist easier to parse in our program by only outputing 1 segment and no endlist marker
|
||||
// anyways this list is only read once and we generate our own.
|
||||
"-hls_list_size", "1",
|
||||
"-hls_flags", "omit_endlist",
|
||||
"-",
|
||||
)
|
||||
|
||||
cmd := exec.Command("ffmpeg", args...)
|
||||
@@ -283,35 +314,49 @@ func (ts *Stream) run(start int32) error {
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
format := filepath.Base(outpath)
|
||||
format := fmt.Sprintf(SegmentNameFormat, encoder_id)
|
||||
should_stop := false
|
||||
is_init_ready := false
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
// ignore m3u8 infos, we only want to know when segments are ready.
|
||||
if line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
|
||||
var segment int32
|
||||
_, _ = fmt.Sscanf(scanner.Text(), format, &segment)
|
||||
|
||||
if segment < start {
|
||||
// This happen because we use -f segments for accurate cutting (since -ss is not)
|
||||
// check comment at begining of function for more info
|
||||
// check comment at beginning of function for more info
|
||||
continue
|
||||
}
|
||||
ts.lock.Lock()
|
||||
|
||||
if !is_init_ready && !ts.isInitReady() {
|
||||
ts.init.encoder = encoder_id
|
||||
close(ts.init.channel)
|
||||
is_init_ready = true
|
||||
}
|
||||
|
||||
ts.heads[encoder_id].segment = segment
|
||||
log.Printf("Segment %d got ready (%d)", segment, encoder_id)
|
||||
if ts.isSegmentReady(segment) {
|
||||
// the current segment is already marked at done so another process has already gone up to here.
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
log.Printf("Killing ffmpeg because segment %d is already ready", segment)
|
||||
log.Printf("Killing ffmpeg %d because segment %d is already ready", encoder_id, segment)
|
||||
should_stop = true
|
||||
} else {
|
||||
ts.segments[segment].encoder = encoder_id
|
||||
close(ts.segments[segment].channel)
|
||||
if segment == end-1 {
|
||||
// file finished, ffmped will finish soon on it's own
|
||||
// file finished, ffmpeg will finish soon on it's own
|
||||
should_stop = true
|
||||
} else if ts.isSegmentReady(segment + 1) {
|
||||
cmd.Process.Signal(os.Interrupt)
|
||||
log.Printf("Killing ffmpeg because next segment %d is ready", segment)
|
||||
log.Printf("Killing ffmpeg %d because next segment %d is ready", encoder_id, segment)
|
||||
should_stop = true
|
||||
}
|
||||
}
|
||||
@@ -330,12 +375,12 @@ func (ts *Stream) run(start int32) error {
|
||||
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 255 {
|
||||
if exiterr, ok := err.(*exec.ExitError); ok && (exiterr.ExitCode() == 255 || exiterr.ExitCode() == -1) {
|
||||
log.Printf("ffmpeg %d was killed by us", encoder_id)
|
||||
} else if err != nil {
|
||||
log.Printf("ffmpeg %d occured an error: %s: %s", encoder_id, err, stderr.String())
|
||||
log.Printf("ffmpeg %d occurred an error: %v: %s", encoder_id, err, stderr.String())
|
||||
} else {
|
||||
log.Printf("ffmpeg %d finished successfully", encoder_id)
|
||||
log.Printf("ffmpeg %d finished successfully, last segment: %d", encoder_id, ts.heads[encoder_id].segment)
|
||||
}
|
||||
|
||||
ts.lock.Lock()
|
||||
@@ -347,34 +392,27 @@ func (ts *Stream) run(start int32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *Stream) GetIndex() (string, error) {
|
||||
// playlist type is event since we can append to the list if Keyframe.IsDone is false.
|
||||
// start time offset makes the stream start at 0s instead of ~3segments from the end (requires version 6 of hls)
|
||||
index := `#EXTM3U
|
||||
#EXT-X-VERSION:6
|
||||
#EXT-X-PLAYLIST-TYPE:EVENT
|
||||
#EXT-X-START:TIME-OFFSET=0
|
||||
#EXT-X-TARGETDURATION:4
|
||||
#EXT-X-MEDIA-SEQUENCE:0
|
||||
#EXT-X-INDEPENDENT-SEGMENTS
|
||||
`
|
||||
length, is_done := ts.file.Keyframes.Length()
|
||||
|
||||
for segment := int32(0); segment < length-1; segment++ {
|
||||
index += fmt.Sprintf("#EXTINF:%.6f\n", ts.file.Keyframes.Get(segment+1)-ts.file.Keyframes.Get(segment))
|
||||
index += fmt.Sprintf("segment-%d.ts\n", segment)
|
||||
func (ts *Stream) GetInit() (string, error) {
|
||||
// No need to lock, the channel won't change.
|
||||
select {
|
||||
case <-ts.init.channel:
|
||||
return fmt.Sprintf(
|
||||
"%s/%s/%s",
|
||||
ts.file.Out,
|
||||
ts.handle.getIdentifier(),
|
||||
fmt.Sprintf(InitNameFormat, ts.init.encoder),
|
||||
), nil
|
||||
case <-time.After(60 * time.Second):
|
||||
return "", errors.New("could not retrieve the selected segment (timeout)")
|
||||
}
|
||||
// do not forget to add the last segment between the last keyframe and the end of the file
|
||||
// if the keyframes extraction is not done, do not bother to add it, it will be retrived on the next index retrival
|
||||
if is_done {
|
||||
index += fmt.Sprintf("#EXTINF:%.6f\n", float64(ts.file.Info.Duration)-ts.file.Keyframes.Get(length-1))
|
||||
index += fmt.Sprintf("segment-%d.ts\n", length-1)
|
||||
index += `#EXT-X-ENDLIST`
|
||||
}
|
||||
return index, nil
|
||||
}
|
||||
|
||||
func (ts *Stream) GetSegment(segment int32) (string, error) {
|
||||
// I was too lazy to put this appart.
|
||||
if segment == -1 {
|
||||
return ts.GetInit()
|
||||
}
|
||||
|
||||
ts.lock.RLock()
|
||||
ready := ts.isSegmentReady(segment)
|
||||
// we want to calculate distance in the same lock else it can be funky
|
||||
@@ -395,23 +433,31 @@ func (ts *Stream) GetSegment(segment int32) (string, error) {
|
||||
if !ready {
|
||||
// Only start a new encode if there is too big a distance between the current encoder and the segment.
|
||||
if distance > 60 || !is_scheduled {
|
||||
log.Printf("Creating new head for %d since closest head is %fs aways", segment, distance)
|
||||
log.Printf("Creating new head for %d since closest head is %fs away", segment, distance)
|
||||
err := ts.run(segment)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
log.Printf("Waiting for segment %d since encoder head is %fs aways", segment, distance)
|
||||
log.Printf("Waiting for segment %d since encoder head is %fs away", segment, distance)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-readyChan:
|
||||
case <-time.After(60 * time.Second):
|
||||
return "", errors.New("could not retrive the selected segment (timeout)")
|
||||
return "", errors.New("could not retrieve the selected segment (timeout)")
|
||||
}
|
||||
}
|
||||
ts.prerareNextSegements(segment)
|
||||
return fmt.Sprintf(ts.handle.getOutPath(ts.segments[segment].encoder), segment), nil
|
||||
return fmt.Sprintf(
|
||||
"%s/%s/%s",
|
||||
ts.file.Out,
|
||||
ts.handle.getIdentifier(),
|
||||
fmt.Sprintf(
|
||||
fmt.Sprintf(SegmentNameFormat, ts.segments[segment].encoder),
|
||||
segment,
|
||||
),
|
||||
), nil
|
||||
}
|
||||
|
||||
func (ts *Stream) prerareNextSegements(segment int32) {
|
||||
@@ -428,7 +474,7 @@ func (ts *Stream) prerareNextSegements(segment int32) {
|
||||
continue
|
||||
}
|
||||
// only start encode for segments not planned (getMinEncoderDistance returns Inf for them)
|
||||
// or if they are 60s away (asume 5s per segments)
|
||||
// or if they are 60s away (assume 5s per segments)
|
||||
if ts.getMinEncoderDistance(i) < 60+(5*float64(i-segment)) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ func (vs *VideoStream) getFlags() Flags {
|
||||
return VideoF
|
||||
}
|
||||
|
||||
func (vs *VideoStream) getOutPath(encoder_id int) string {
|
||||
return fmt.Sprintf("%s/segment-%s-%d-%%d.ts", vs.file.Out, vs.quality, encoder_id)
|
||||
func (vs *VideoStream) getIdentifier() string {
|
||||
return fmt.Sprintf("%s", vs.quality)
|
||||
}
|
||||
|
||||
func closestMultiple(n int32, x int32) int32 {
|
||||
@@ -72,3 +72,31 @@ func (vs *VideoStream) getTranscodeArgs(segments string) []string {
|
||||
)
|
||||
return args
|
||||
}
|
||||
|
||||
func (ts *VideoStream) GetIndex() (string, error) {
|
||||
// playlist type is event since we can append to the list if Keyframe.IsDone is false.
|
||||
// start time offset makes the stream start at 0s instead of ~3segments from the end (requires version 6 of hls)
|
||||
index := `#EXTM3U
|
||||
#EXT-X-VERSION:7
|
||||
#EXT-X-PLAYLIST-TYPE:EVENT
|
||||
#EXT-X-START:TIME-OFFSET=0
|
||||
#EXT-X-MEDIA-SEQUENCE:0
|
||||
#EXT-X-INDEPENDENT-SEGMENTS
|
||||
#EXT-X-MAP:URI="init.mp4"
|
||||
`
|
||||
index += fmt.Sprintf("#EXT-X-TARGETDURATION:%d\n", int(OptimalFragmentDuration)+1)
|
||||
length, is_done := ts.file.Keyframes.Length()
|
||||
|
||||
for segment := int32(0); segment < length-1; segment++ {
|
||||
index += fmt.Sprintf("#EXTINF:%.6f\n", ts.file.Keyframes.Get(segment+1)-ts.file.Keyframes.Get(segment))
|
||||
index += fmt.Sprintf("segment-%d.m4s\n", segment)
|
||||
}
|
||||
// do not forget to add the last segment between the last keyframe and the end of the file
|
||||
// if the keyframes extraction is not done, do not bother to add it, it will be retrieval on the next index retrieval
|
||||
if is_done {
|
||||
index += fmt.Sprintf("#EXTINF:%.6f\n", float64(ts.file.Info.Duration)-ts.file.Keyframes.Get(length-1))
|
||||
index += fmt.Sprintf("segment-%d.m4s\n", length-1)
|
||||
index += `#EXT-X-ENDLIST`
|
||||
}
|
||||
return index, nil
|
||||
}
|
||||
|
||||
@@ -71,8 +71,11 @@ func GetClientId(c echo.Context) (string, error) {
|
||||
}
|
||||
|
||||
func ParseSegment(segment string) (int32, error) {
|
||||
if segment == "init.mp4" {
|
||||
return -1, nil
|
||||
}
|
||||
var ret int32
|
||||
_, err := fmt.Sscanf(segment, "segment-%d.ts", &ret)
|
||||
_, err := fmt.Sscanf(segment, "segment-%d.m4s", &ret)
|
||||
if err != nil {
|
||||
return 0, echo.NewHTTPError(http.StatusBadRequest, "Could not parse segment.")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user