298 lines
6.8 KiB
Go
298 lines
6.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"stream-poc2/ffmpeg"
|
|
"stream-poc2/model"
|
|
"strings"
|
|
)
|
|
|
|
func isFile(path string) bool {
|
|
stat, err := os.Stat(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return !stat.IsDir()
|
|
}
|
|
|
|
func isDir(path string) bool {
|
|
stat, err := os.Stat(path)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return stat.IsDir()
|
|
}
|
|
|
|
func createRawStructure(root string, audioStreams []int) error {
|
|
dirs := []string{
|
|
"",
|
|
"video",
|
|
"audio",
|
|
"cache",
|
|
"cache/video",
|
|
}
|
|
|
|
for _, dir := range dirs {
|
|
log.Println("mkdir:", dir)
|
|
err := os.MkdirAll(path.Join(root, dir), 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, streamIdx := range audioStreams {
|
|
dir := path.Join(root, "audio", strconv.Itoa(streamIdx))
|
|
log.Println("mkdir:", dir)
|
|
err := os.MkdirAll(dir, 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func writeJSON(fpath string, data any) error {
|
|
log.Println("writing", fpath)
|
|
f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func(f *os.File) {
|
|
err := f.Close()
|
|
if err != nil {
|
|
log.Println("Failed to close", fpath, err)
|
|
}
|
|
}(f)
|
|
|
|
err = json.NewEncoder(f).Encode(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func importMain(filepath, target string) {
|
|
targetFullPath := path.Join("db", target)
|
|
|
|
if isDir(targetFullPath) {
|
|
fmt.Println("target already exists")
|
|
return
|
|
}
|
|
|
|
if !isFile(filepath) {
|
|
fmt.Println("Source does not exists")
|
|
return
|
|
}
|
|
|
|
log.Println("Running FFProbe...")
|
|
|
|
ffProbed, err := ffmpeg.FFProbe(filepath)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var numVideoStreams int
|
|
|
|
// we will fill those out from ffprobe's output
|
|
nativeVideoStream := model.VideoStream{}
|
|
var audioStreams []model.AudioStream
|
|
|
|
for _, stream := range ffProbed.Streams {
|
|
log.Println("Discovered stream:", stream.Index, stream.CodecType)
|
|
if stream.CodecType == "audio" {
|
|
lang, ok := stream.Tags["language"]
|
|
if !ok {
|
|
lang = "eng" // use as default
|
|
}
|
|
title, _ := stream.Tags["title"]
|
|
if !ok {
|
|
title = fmt.Sprintf("Unknown (%d)", stream.Index) // Title is used as name which must be different
|
|
}
|
|
|
|
sampleRate, err := strconv.Atoi(stream.SampleRate)
|
|
if err != nil {
|
|
log.Println("!!! Couldn't get audio sample rate !!!")
|
|
}
|
|
|
|
bitRate, err := strconv.Atoi(stream.BitRate)
|
|
if err != nil {
|
|
log.Println("!!! Couldn't get audio bit rate !!!")
|
|
}
|
|
|
|
audioStreams = append(audioStreams, model.AudioStream{
|
|
Index: stream.Index,
|
|
Lang: lang,
|
|
Title: title,
|
|
Default: stream.Disposition.Default == 1,
|
|
Codec: stream.CodecName,
|
|
SampleRate: sampleRate,
|
|
Bitrate: bitRate,
|
|
Channels: stream.Channels,
|
|
AudioChunks: nil,
|
|
})
|
|
}
|
|
if stream.CodecType == "video" {
|
|
// funnily mkv can encode any sort of files, including "cover images", those represented as video frames.
|
|
frameRateParts := strings.Split(stream.AvgFrameRate, "/")
|
|
a, err := strconv.Atoi(frameRateParts[0])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
b, err := strconv.Atoi(frameRateParts[1])
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
if a > 0 && b > 0 {
|
|
nativeVideoStream.FrameRate = float64(a) / float64(b)
|
|
}
|
|
|
|
if nativeVideoStream.FrameRate == 0 || strings.HasPrefix(stream.Tags["mimetype"], "image") {
|
|
log.Println("This is a still image, ignoring...")
|
|
continue
|
|
}
|
|
|
|
numVideoStreams++
|
|
nativeVideoStream.Resolution = model.Resolution{
|
|
Width: stream.Width,
|
|
Height: stream.Height,
|
|
}
|
|
if stream.BitRate != "" {
|
|
nativeVideoStream.PeakBitrate, err = strconv.Atoi(stream.BitRate)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
nativeVideoStream.AvgBitrate, err = strconv.Atoi(stream.BitRate)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
} else {
|
|
log.Println("Warning: couldn't extract video bitrate")
|
|
}
|
|
|
|
nativeVideoStream.HLSCodecsString, _ = ffmpeg.FigureOutHLSCodecsParam(stream) // ignore if it's unrecognizable..
|
|
}
|
|
|
|
}
|
|
|
|
if numVideoStreams != 1 {
|
|
log.Printf("!!! The input media should have exactly one video stream, this has %d !!!\n", numVideoStreams)
|
|
}
|
|
|
|
metadata := model.MediaMetadata{
|
|
FriendlyName: path.Base(filepath),
|
|
TargetDuration: ffmpeg.SlicerTargetDurationSec,
|
|
VideoStream: nativeVideoStream,
|
|
AudioStreams: audioStreams,
|
|
}
|
|
|
|
audioStreamIndexes := make([]int, len(audioStreams))
|
|
for i, audioStream := range audioStreams {
|
|
audioStreamIndexes[i] = audioStream.Index
|
|
}
|
|
|
|
// create structure
|
|
err = createRawStructure(targetFullPath, audioStreamIndexes)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// write ffprobeOutput
|
|
err = writeJSON(path.Join(targetFullPath, "ffprobe.json"), ffProbed)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
log.Println("Now segmenting...")
|
|
segmentsVideoCSV, audioSegmentsCSVs, err := ffmpeg.MakeSegments(filepath, targetFullPath, audioStreams)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
log.Println("Segment data: ", segmentsVideoCSV, audioSegmentsCSVs)
|
|
|
|
log.Println("Now processing segment data...")
|
|
|
|
// read slice stuff
|
|
segments, err := ffmpeg.ReadSegmentsCSV(segmentsVideoCSV)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// load videoStream chunks
|
|
videoChunks := make([]model.Chunk, len(segments))
|
|
for i, segment := range segments {
|
|
videoChunks[i] = model.Chunk{
|
|
FName: segment.Name,
|
|
Length: segment.Len(),
|
|
}
|
|
}
|
|
|
|
metadata.VideoStream.VideoChunks = videoChunks
|
|
|
|
// load audioStream chunks
|
|
for i, slicesFile := range audioSegmentsCSVs {
|
|
segments, err = ffmpeg.ReadSegmentsCSV(slicesFile)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
audioChunks := make([]model.Chunk, len(segments))
|
|
for j, segment := range segments {
|
|
audioChunks[j] = model.Chunk{
|
|
FName: segment.Name,
|
|
Length: segment.Len(),
|
|
}
|
|
}
|
|
metadata.AudioStreams[i].AudioChunks = audioChunks
|
|
}
|
|
|
|
log.Println("Writing metadata...")
|
|
|
|
// write metadata
|
|
err = writeJSON(path.Join(targetFullPath, "meta.json"), metadata)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// pre-encode
|
|
log.Println("Pre-encoding some slices...")
|
|
|
|
for i, chunk := range videoChunks {
|
|
// encode 7 slices to begin with...
|
|
if i > 6 {
|
|
break
|
|
}
|
|
|
|
origSegmentPath := path.Join(targetFullPath, "video", chunk.FName)
|
|
for profileName, profileSettings := range model.VideoProfiles {
|
|
preEncodedSegmentDir := path.Join(targetFullPath, "cache", "video", profileName)
|
|
err = os.MkdirAll(preEncodedSegmentDir, 0o755)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
preEncodedSegmentPath := path.Join(preEncodedSegmentDir, chunk.FName)
|
|
log.Println(origSegmentPath, " -> ", preEncodedSegmentPath)
|
|
|
|
var f *os.File
|
|
f, err = os.OpenFile(preEncodedSegmentPath, os.O_WRONLY|os.O_CREATE, 0o644)
|
|
if err != nil {
|
|
log.Println("failed to open chunk", err)
|
|
panic(err)
|
|
}
|
|
|
|
err = ffmpeg.EncodeChunk(profileSettings, nativeVideoStream.Resolution.Height, "", origSegmentPath, f)
|
|
_ = f.Close()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|