init
This commit is contained in:
commit
fa47df6d89
22 changed files with 1724 additions and 0 deletions
237
handlersMedia.go
Normal file
237
handlersMedia.go
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"stream-poc2/ffmpeg"
|
||||
"stream-poc2/model"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func mediaListingHandler(ctx *gin.Context) {
|
||||
entries, err := os.ReadDir("db")
|
||||
if err != nil {
|
||||
log.Println("Failed to list dir", err)
|
||||
ctx.Status(500)
|
||||
return
|
||||
}
|
||||
|
||||
var medias []model.APIMedia
|
||||
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
var m model.MediaMetadata
|
||||
m, err = loadMediaMetadata(e.Name())
|
||||
if err != nil {
|
||||
log.Println("Failed to load metadata for", e)
|
||||
continue
|
||||
}
|
||||
medias = append(medias, model.APIMedia{
|
||||
Name: e.Name(),
|
||||
FriendlyName: m.FriendlyName,
|
||||
})
|
||||
}
|
||||
|
||||
log.Println("loaded", medias)
|
||||
ctx.JSON(200, medias)
|
||||
}
|
||||
|
||||
func metadataHandler(ctx *gin.Context) {
|
||||
_, m := getMetadata(ctx)
|
||||
ctx.JSON(http.StatusOK, m)
|
||||
}
|
||||
|
||||
func masterPlaylistHandler(ctx *gin.Context) {
|
||||
mediaID, metadata := getMetadata(ctx)
|
||||
|
||||
masterPlaylist := fmt.Sprintf("#EXTM3U\n\n# Master playlist for %s\n\n", mediaID)
|
||||
|
||||
for profile, profileSettings := range model.VideoProfiles {
|
||||
masterPlaylist += fmt.Sprintf(
|
||||
"#EXT-X-STREAM-INF:BANDWIDTH=%d,AVERAGE-BANDWIDTH=%d,CODECS=\"%s\",RESOLUTION=%s,FRAME-RATE=%.6f,AUDIO=\"audios\",NAME=\"%s\",STABLE-VARIANT-ID\"%s\"\n",
|
||||
profileSettings.MaxBitrate,
|
||||
profileSettings.AvgBitrate,
|
||||
profileSettings.HLSCodec(),
|
||||
profileSettings.Resolution.String(),
|
||||
metadata.VideoStream.FrameRate,
|
||||
profile, profile,
|
||||
)
|
||||
masterPlaylist += fmt.Sprintf("video/%s/playlist.m3u8\n\n", profile)
|
||||
}
|
||||
|
||||
profile := "orig"
|
||||
masterPlaylist += fmt.Sprintf(
|
||||
"#EXT-X-STREAM-INF:BANDWIDTH=%d,AVERAGE-BANDWIDTH=%d,CODECS=\"%s\",RESOLUTION=%s,FRAME-RATE=%.6f,AUDIO=\"audios\",NAME=\"%s\",STABLE-VARIANT-ID\"%s\"\n",
|
||||
metadata.VideoStream.PeakBitrate,
|
||||
metadata.VideoStream.AvgBitrate,
|
||||
metadata.VideoStream.HLSCodecsString,
|
||||
metadata.VideoStream.Resolution,
|
||||
metadata.VideoStream.FrameRate,
|
||||
profile, profile,
|
||||
)
|
||||
masterPlaylist += fmt.Sprintf("video/%s/playlist.m3u8\n\n", profile)
|
||||
|
||||
defaultMarked := false
|
||||
for _, audioStream := range metadata.AudioStreams {
|
||||
autoSelect := ""
|
||||
if audioStream.Default && !defaultMarked {
|
||||
autoSelect = ",AUTOSELECT=YES,DEFAULT=YES"
|
||||
defaultMarked = true
|
||||
}
|
||||
audioStreamPath := fmt.Sprintf("audio/%d/playlist.m3u8", audioStream.Index)
|
||||
masterPlaylist += fmt.Sprintf(
|
||||
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audios\",NAME=\"%s\",LANGUAGE=\"%s\"%s,CHANNELS=\"%d\",SAMPLE-RATE=%d,URI=\"%s\"\n",
|
||||
audioStream.Title, audioStream.Lang, autoSelect, audioStream.Channels, audioStream.SampleRate, audioStreamPath,
|
||||
)
|
||||
}
|
||||
|
||||
ctx.Data(200, "application/vnd.apple.mpegurl", []byte(masterPlaylist))
|
||||
}
|
||||
|
||||
func videoPlaylistHandler(ctx *gin.Context) {
|
||||
_, metadata := getMetadata(ctx)
|
||||
|
||||
playlist := fmt.Sprintf(`#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-TARGETDURATION:%f
|
||||
#EXT-X-MEDIA-SEQUENCE:0
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
`, metadata.TargetDuration)
|
||||
|
||||
for i, chunk := range metadata.VideoStream.VideoChunks {
|
||||
lenStr := strconv.FormatFloat(chunk.Length, 'f', 6, 64)
|
||||
playlist += fmt.Sprintf("#EXTINF:%s,\n", lenStr)
|
||||
playlist += fmt.Sprintf("s%d.ts\n", i)
|
||||
}
|
||||
|
||||
playlist += "#EXT-X-ENDLIST"
|
||||
ctx.Data(200, "application/vnd.apple.mpegurl", []byte(playlist))
|
||||
}
|
||||
|
||||
func audioPlaylistHandler(ctx *gin.Context) {
|
||||
_, metadata := getMetadata(ctx)
|
||||
idxStr := ctx.Param("idx")
|
||||
idx, err := strconv.Atoi(idxStr)
|
||||
if err != nil {
|
||||
log.Println("Invalid index")
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
|
||||
var langChunks []model.Chunk
|
||||
|
||||
for _, audioStream := range metadata.AudioStreams {
|
||||
if audioStream.Index == idx {
|
||||
langChunks = audioStream.AudioChunks
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if langChunks == nil {
|
||||
log.Println("index not found")
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
|
||||
playlist := fmt.Sprintf(`#EXTM3U
|
||||
#EXT-X-VERSION:3
|
||||
#EXT-X-TARGETDURATION:%f
|
||||
#EXT-X-MEDIA-SEQUENCE:0
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
`, metadata.TargetDuration)
|
||||
for i, chunk := range langChunks {
|
||||
lenStr := strconv.FormatFloat(chunk.Length, 'f', 6, 64)
|
||||
playlist += fmt.Sprintf("#EXTINF:%s,\n", lenStr)
|
||||
playlist += fmt.Sprintf("s%d.ts\n", i)
|
||||
}
|
||||
|
||||
playlist += "#EXT-X-ENDLIST"
|
||||
ctx.Data(200, "application/vnd.apple.mpegurl", []byte(playlist))
|
||||
}
|
||||
|
||||
// serveChunk must be the last function call in the branch
|
||||
func serveChunk(ctx *gin.Context, chunkPath string) {
|
||||
stat, err := os.Stat(chunkPath)
|
||||
if err != nil {
|
||||
log.Printf("failed to stat chunk %s: %s", chunkPath, err)
|
||||
if os.IsNotExist(err) {
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
ctx.Status(500)
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(chunkPath, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
log.Printf("failed to open chunk %s: %s", chunkPath, err)
|
||||
ctx.Status(500)
|
||||
return
|
||||
}
|
||||
defer func(f *os.File) {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
log.Println("failed to close chunk:", err)
|
||||
}
|
||||
}(f)
|
||||
ctx.DataFromReader(200, stat.Size(), "video/MP2T; charset=binary", f, nil)
|
||||
}
|
||||
|
||||
func videoSegmentHandler(ctx *gin.Context) {
|
||||
mediaID, metadata := getMetadata(ctx)
|
||||
|
||||
profile := ctx.Param("profile")
|
||||
segment := ctx.Param("segment")
|
||||
origSegmentPath := path.Join("db", mediaID, "video", segment)
|
||||
|
||||
// quick path for identity stream
|
||||
if profile == "orig" {
|
||||
log.Println("Serving identity")
|
||||
serveChunk(ctx, origSegmentPath)
|
||||
return
|
||||
}
|
||||
|
||||
profileSettings, ok := model.VideoProfiles[profile]
|
||||
if !ok {
|
||||
log.Println("invalid profile")
|
||||
ctx.Status(404)
|
||||
return
|
||||
}
|
||||
|
||||
preEncodedSegmentPath := path.Join("db", mediaID, "cache", "video", profile, segment)
|
||||
if isFile(preEncodedSegmentPath) {
|
||||
log.Println("Serving from cache")
|
||||
serveChunk(ctx, preEncodedSegmentPath)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Serving transcode")
|
||||
|
||||
// transcode video
|
||||
ctx.Writer.Header().Set("Content-Type", "video/MP2T; charset=binary")
|
||||
ctx.Writer.WriteHeader(200)
|
||||
|
||||
encodeStartTime := time.Now()
|
||||
err := ffmpeg.EncodeChunk(profileSettings, metadata.VideoStream.Resolution.Height, getCurrentPreset(), origSegmentPath, ctx.Writer)
|
||||
if err != nil {
|
||||
log.Println("!!! WARNING: Something went wrong while encoding: ", err)
|
||||
}
|
||||
log.Println("transcode took ", time.Since(encodeStartTime))
|
||||
}
|
||||
|
||||
func audioSegmentHandler(ctx *gin.Context) {
|
||||
mediaID, _ := getMetadata(ctx)
|
||||
idx := ctx.Param("idx")
|
||||
segment := ctx.Param("segment")
|
||||
|
||||
origSegmentPath := path.Join("db", mediaID, "audio", idx, segment)
|
||||
|
||||
serveChunk(ctx, origSegmentPath)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue