mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add keyframe parsing
This commit is contained in:
parent
512aae252c
commit
570f08755d
@ -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
|
||||
)
|
||||
|
@ -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=
|
||||
|
@ -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"))
|
||||
}
|
||||
|
97
transcoder/transcoder/filestream.go
Normal file
97
transcoder/transcoder/filestream.go
Normal 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
|
||||
}
|
39
transcoder/transcoder/quality.go
Normal file
39
transcoder/transcoder/quality.go
Normal 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")
|
||||
}
|
9
transcoder/transcoder/stream.go
Normal file
9
transcoder/transcoder/stream.go
Normal 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
|
||||
}
|
18
transcoder/transcoder/transcoder.go
Normal file
18
transcoder/transcoder/transcoder.go
Normal 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()
|
||||
}
|
@ -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)}})
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user