Add keyframe parsing

This commit is contained in:
Zoe Roux 2024-01-11 23:06:03 +01:00
parent 512aae252c
commit 570f08755d
8 changed files with 227 additions and 10 deletions

View File

@ -3,6 +3,7 @@ module github.com/zoriya/kyoo/transcoder
go 1.20
require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/labstack/echo/v4 v4.11.4 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
@ -13,4 +14,5 @@ require (
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
)

View File

@ -1,3 +1,5 @@
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
@ -21,3 +23,5 @@ golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=

View File

@ -1,16 +1,19 @@
package main
import (
"fmt"
"net/http"
"github.com/zoriya/kyoo/transcoder/transcoder"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// Direct video
//
// Retrieve the raw video stream, in the same container as the one on the server. No transcoding or
// transmuxing is done.
//
// Path: /:resource/:slug/direct
func DirectStream(c echo.Context) error {
resource := c.Param("resource")
@ -23,23 +26,47 @@ func DirectStream(c echo.Context) error {
return c.File(*path)
}
func ErrorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
} else {
c.Logger().Error(err)
// Get master playlist
//
// Get a master playlist containing all possible video qualities and audios available for this resource.
// Note that the direct stream is missing (since the direct is not an hls stream) and
// subtitles/fonts are not included to support more codecs than just webvtt.
//
// Path: /:resource/:slug/master.m3u8
func (h *Handler) GetMaster(c echo.Context) error {
resource := c.Param("resource")
slug := c.Param("slug")
client, err := GetClientId(c)
if err != nil {
return err
}
c.JSON(code, struct {
Errors []string `json:"errors"`
}{Errors: []string{fmt.Sprint(err)}})
path, err := GetPath(resource, slug)
if err != nil {
return err
}
ret, err := h.transcoder.GetMaster(*path, *client)
if err != nil {
return err
}
return c.String(http.StatusOK, ret)
}
type Handler struct {
transcoder *transcoder.Transcoder
}
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.HTTPErrorHandler = ErrorHandler
h := Handler{}
e.GET("/:resource/:slug/direct", DirectStream)
e.GET("/:resource/:slug/master.m3u8", h.GetMaster)
e.Logger.Fatal(e.Start(":7666"))
}

View File

@ -0,0 +1,97 @@
package transcoder
import (
"math"
"os/exec"
"strconv"
"strings"
)
type FileStream struct {
Path string
Keyframes []float64
CanTransmux bool
streams map[Quality]TranscodeStream
}
func NewFileStream(path string) (*FileStream, error) {
keyframes, can_transmux, err := GetKeyframes(path)
if err != nil {
return nil, err
}
return &FileStream{
Path: path,
Keyframes: keyframes,
CanTransmux: can_transmux,
streams: make(map[Quality]TranscodeStream),
}, nil
}
func GetKeyframes(path string) ([]float64, bool, error) {
// run ffprobe to return all IFrames, IFrames are points where we can split the video in segments.
out, err := exec.Command(
"ffprobe",
"-loglevel", "error",
"-select_streams", "v:0",
"-show_entries", "packet=pts_time,flags",
"-of", "csv=print_section=0",
path,
).Output()
// 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
// since ffmpeg parses every frames when this flag is set.
if err != nil {
return nil, false, err
}
ret := make([]float64, 0, 300)
last := 0.
can_transmux := true
for _, frame := range strings.Split(string(out), "\n") {
x := strings.Split(frame, ",")
pts, flags := x[0], x[1]
// Only take keyframes
if flags[0] != 'K' {
continue
}
fpts, err := strconv.ParseFloat(pts, 64)
if err != nil {
return nil, false, err
}
// Only save keyframes with at least 3s betweens, we dont want a segment of 0.2s
if fpts-last < 3 {
continue
}
// If we have a segment of more than 20s, create new keyframes during transcode and disable transmuxing
if fpts-last > 20 {
can_transmux = false
fake_count := math.Ceil(fpts - last/4)
duration := (fpts - last) / fake_count
// let the last one be handled normally, this prevents floating points rounding
for fake_count > 1 {
fake_count--
last = last + duration
ret = append(ret, last)
}
}
last = fpts
ret = append(ret, fpts)
}
return ret, can_transmux, nil
}
func (fs *FileStream) IsDead() bool {
for _, s := range fs.streams {
if len(s.Clients) > 0 {
return true
}
}
return false
}

View File

@ -0,0 +1,39 @@
package transcoder
type Quality int8
const (
P240 Quality = iota
P360
P480
P720
P1080
P1440
P4k
P8k
Original
)
func (q Quality) String() string {
switch q {
case P240:
return "240p"
case P360:
return "360p"
case P480:
return "480p"
case P720:
return "720p"
case P1080:
return "1080p"
case P1440:
return "1440p"
case P4k:
return "4k"
case P8k:
return "8k"
case Original:
return "Original"
}
panic("Invalid quality value")
}

View File

@ -0,0 +1,9 @@
package transcoder
type TranscodeStream struct {
File FileStream
Clients []string
// true if the segment at given index is completed/transcoded, false otherwise
segments []bool
// TODO: add ffmpeg process
}

View File

@ -0,0 +1,18 @@
package transcoder
type Transcoder struct {
// All file streams currently running, index is file path
streams map[string]FileStream
}
func (t *Transcoder) GetMaster(path string, client string) (*string, error) {
stream, ok := t.streams[path]
if !ok {
stream, err := NewFileStream(path)
if err != nil {
return nil, err
}
t.streams[path] = *stream
}
return &stream.GetMaster()
}

View File

@ -55,3 +55,24 @@ func GetPath(resource string, slug string) (*string, error) {
return &ret.Path, nil
}
func GetClientId(c echo.Context) (*string, error) {
key := c.Request().Header.Get("X-CLIENT-ID")
if key == "" {
return nil, errors.New("Missing client id. Please specify the X-CLIENT-ID header to a guid constant for the lifetime of the player (but unique per instance).")
}
return &key, nil
}
func ErrorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
} else {
c.Logger().Error(err)
}
c.JSON(code, struct {
Errors []string `json:"errors"`
}{Errors: []string{fmt.Sprint(err)}})
}