package transcoder import ( "context" "fmt" "math" "os" "path/filepath" "seanime/internal/mediastream/videofile" "seanime/internal/util/result" "strings" "sync" "time" "github.com/rs/zerolog" ) // FileStream represents a stream of file data. // It holds the keyframes, media information, video streams, and audio streams. type FileStream struct { ready sync.WaitGroup // A WaitGroup to synchronize go routines. err error // An error that might occur during processing. Path string // The path of the file. Out string // The output path. Keyframes *Keyframe // The keyframes of the video. Info *videofile.MediaInfo // The media information of the file. videos *result.Map[Quality, *VideoStream] // A map of video streams. audios *result.Map[int32, *AudioStream] // A map of audio streams. logger *zerolog.Logger settings *Settings } // NewFileStream creates a new FileStream. func NewFileStream( path string, sha string, mediaInfo *videofile.MediaInfo, settings *Settings, logger *zerolog.Logger, ) *FileStream { ret := &FileStream{ Path: path, Out: filepath.Join(settings.StreamDir, sha), videos: result.NewResultMap[Quality, *VideoStream](), audios: result.NewResultMap[int32, *AudioStream](), logger: logger, settings: settings, Info: mediaInfo, } ret.ready.Add(1) go func() { defer ret.ready.Done() ret.Keyframes = GetKeyframes(path, sha, logger, settings) }() return ret } // Kill stops all streams. func (fs *FileStream) Kill() { fs.videos.Range(func(_ Quality, s *VideoStream) bool { s.Kill() return true }) fs.audios.Range(func(_ int32, s *AudioStream) bool { s.Kill() return true }) } // Destroy stops all streams and removes the output directory. func (fs *FileStream) Destroy() { fs.logger.Debug().Msg("filestream: Destroying streams") fs.Kill() _ = os.RemoveAll(fs.Out) } // GetMaster generates the master playlist. func (fs *FileStream) GetMaster() string { master := "#EXTM3U\n" if fs.Info.Video != nil { var transmuxQuality Quality for _, quality := range Qualities { if quality.Height() >= fs.Info.Video.Quality.Height() || quality.AverageBitrate() >= fs.Info.Video.Bitrate { transmuxQuality = quality break } } { bitrate := float64(fs.Info.Video.Bitrate) master += "#EXT-X-STREAM-INF:" master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", int(math.Min(bitrate*0.8, float64(transmuxQuality.AverageBitrate())))) master += fmt.Sprintf("BANDWIDTH=%d,", int(math.Min(bitrate, float64(transmuxQuality.MaxBitrate())))) master += fmt.Sprintf("RESOLUTION=%dx%d,", fs.Info.Video.Width, fs.Info.Video.Height) if fs.Info.Video.MimeCodec != nil { master += fmt.Sprintf("CODECS=\"%s\",", *fs.Info.Video.MimeCodec) } master += "AUDIO=\"audio\"," master += "CLOSED-CAPTIONS=NONE\n" master += fmt.Sprintf("./%s/index.m3u8\n", Original) } aspectRatio := float32(fs.Info.Video.Width) / float32(fs.Info.Video.Height) // codec is the prefix + the level, the level is not part of the codec we want to compare for the same_codec check bellow transmuxPrefix := "avc1.6400" transmuxCodec := transmuxPrefix + "28" for _, quality := range Qualities { sameCodec := fs.Info.Video.MimeCodec != nil && strings.HasPrefix(*fs.Info.Video.MimeCodec, transmuxPrefix) includeLvl := quality.Height() < fs.Info.Video.Quality.Height() || (quality.Height() == fs.Info.Video.Quality.Height() && !sameCodec) if includeLvl { master += "#EXT-X-STREAM-INF:" master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", quality.AverageBitrate()) master += fmt.Sprintf("BANDWIDTH=%d,", quality.MaxBitrate()) master += fmt.Sprintf("RESOLUTION=%dx%d,", int(aspectRatio*float32(quality.Height())+0.5), quality.Height()) master += fmt.Sprintf("CODECS=\"%s\",", transmuxCodec) master += "AUDIO=\"audio\"," master += "CLOSED-CAPTIONS=NONE\n" master += fmt.Sprintf("./%s/index.m3u8\n", quality) } } //for _, quality := range Qualities { // if quality.Height() < fs.Info.Video.Quality.Height() && quality.AverageBitrate() < fs.Info.Video.Bitrate { // master += "#EXT-X-STREAM-INF:" // master += fmt.Sprintf("AVERAGE-BANDWIDTH=%d,", quality.AverageBitrate()) // master += fmt.Sprintf("BANDWIDTH=%d,", quality.MaxBitrate()) // master += fmt.Sprintf("RESOLUTION=%dx%d,", int(aspectRatio*float32(quality.Height())+0.5), quality.Height()) // master += "CODECS=\"avc1.640028\"," // master += "AUDIO=\"audio\"," // master += "CLOSED-CAPTIONS=NONE\n" // master += fmt.Sprintf("./%s/index.m3u8\n", quality) // } //} } for _, audio := range fs.Info.Audios { master += "#EXT-X-MEDIA:TYPE=AUDIO," master += "GROUP-ID=\"audio\"," if audio.Language != nil { master += fmt.Sprintf("LANGUAGE=\"%s\",", *audio.Language) } if audio.Title != nil { master += fmt.Sprintf("NAME=\"%s\",", *audio.Title) } else if audio.Language != nil { master += fmt.Sprintf("NAME=\"%s\",", *audio.Language) } else { master += fmt.Sprintf("NAME=\"Audio %d\",", audio.Index) } if audio.IsDefault { master += "DEFAULT=YES," } master += "CHANNELS=\"2\"," master += fmt.Sprintf("URI=\"./audio/%d/index.m3u8\"\n", audio.Index) } return master } // GetVideoIndex gets the index of a video stream of a specific quality. func (fs *FileStream) GetVideoIndex(quality Quality) (string, error) { stream := fs.getVideoStream(quality) return stream.GetIndex() } // getVideoStream gets a video stream of a specific quality. // It creates a new stream if it does not exist. func (fs *FileStream) getVideoStream(quality Quality) *VideoStream { stream, _ := fs.videos.GetOrSet(quality, func() (*VideoStream, error) { return NewVideoStream(fs, quality, fs.logger, fs.settings), nil }) return stream } // GetVideoSegment gets a segment of a video stream of a specific quality. //func (fs *FileStream) GetVideoSegment(quality Quality, segment int32) (string, error) { // stream := fs.getVideoStream(quality) // return stream.GetSegment(segment) //} // GetVideoSegment gets a segment of a video stream of a specific quality. func (fs *FileStream) GetVideoSegment(quality Quality, segment int32) (string, error) { streamLogger.Debug().Msgf("filestream: Retrieving video segment %d (%s)", segment, quality) // Debug ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() debugStreamRequest(fmt.Sprintf("video %s, segment %d", quality, segment), ctx) //stream := fs.getVideoStream(quality) //return stream.GetSegment(segment) // Channel to signal completion done := make(chan struct{}) var ret string var err error // Execute the retrieval operation in a goroutine go func() { defer close(done) stream := fs.getVideoStream(quality) ret, err = stream.GetSegment(segment) }() // Wait for either the operation to complete or the timeout to occur select { case <-done: return ret, err case <-ctx.Done(): return "", fmt.Errorf("filestream: timeout while retrieving video segment %d (%s)", segment, quality) } } // GetAudioIndex gets the index of an audio stream of a specific index. func (fs *FileStream) GetAudioIndex(audio int32) (string, error) { stream := fs.getAudioStream(audio) return stream.GetIndex() } // GetAudioSegment gets a segment of an audio stream of a specific index. func (fs *FileStream) GetAudioSegment(audio int32, segment int32) (string, error) { streamLogger.Debug().Msgf("filestream: Retrieving audio %d segment %d", audio, segment) // Debug ctx, cancel := context.WithCancel(context.Background()) defer cancel() debugStreamRequest(fmt.Sprintf("audio %d, segment %d", audio, segment), ctx) stream := fs.getAudioStream(audio) return stream.GetSegment(segment) } // getAudioStream gets an audio stream of a specific index. // It creates a new stream if it does not exist. func (fs *FileStream) getAudioStream(audio int32) *AudioStream { stream, _ := fs.audios.GetOrSet(audio, func() (*AudioStream, error) { return NewAudioStream(fs, audio, fs.logger, fs.settings), nil }) return stream } func debugStreamRequest(text string, ctx context.Context) { //ctx, cancel := context.WithCancel(context.Background()) //defer cancel() if debugStream { start := time.Now() ticker := time.NewTicker(2 * time.Second) go func() { for { select { case <-ctx.Done(): ticker.Stop() return case <-ticker.C: if debugStream { time.Sleep(2 * time.Second) streamLogger.Debug().Msgf("t: %s has been running for %.2f", text, time.Since(start).Seconds()) } } } }() } }