2020-07-10 08:45:58 -08:00
package core
2020-02-03 07:54:59 -09:00
import (
"context"
2020-02-19 10:53:35 -09:00
"fmt"
2020-02-03 07:54:59 -09:00
"io"
2020-02-24 09:31:05 -09:00
"mime"
2020-02-03 07:54:59 -09:00
"os"
2020-10-27 13:02:20 -08:00
"sync"
2020-02-03 07:54:59 -09:00
"time"
"github.com/navidrome/navidrome/conf"
2020-02-21 05:36:29 -09:00
"github.com/navidrome/navidrome/consts"
2022-12-20 08:25:47 -09:00
"github.com/navidrome/navidrome/core/ffmpeg"
2020-02-03 07:54:59 -09:00
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
2020-05-13 12:49:55 -08:00
"github.com/navidrome/navidrome/model/request"
2021-02-09 07:33:26 -09:00
"github.com/navidrome/navidrome/utils/cache"
2020-02-03 07:54:59 -09:00
)
type MediaStreamer interface {
2020-03-16 10:28:13 -08:00
NewStream ( ctx context . Context , id string , reqFormat string , reqBitRate int ) ( * Stream , error )
2022-12-18 08:12:37 -09:00
DoStream ( ctx context . Context , mf * model . MediaFile , reqFormat string , reqBitRate int ) ( * Stream , error )
2020-02-03 07:54:59 -09:00
}
2020-10-23 17:30:45 -08:00
type TranscodingCache cache . FileCache
2020-07-24 11:40:27 -08:00
2022-12-20 08:25:47 -09:00
func NewMediaStreamer ( ds model . DataStore , t ffmpeg . FFmpeg , cache TranscodingCache ) MediaStreamer {
2021-07-21 08:46:30 -08:00
return & mediaStreamer { ds : ds , transcoder : t , cache : cache }
2020-02-03 07:54:59 -09:00
}
type mediaStreamer struct {
2021-07-21 08:46:30 -08:00
ds model . DataStore
2022-12-20 08:25:47 -09:00
transcoder ffmpeg . FFmpeg
2021-07-21 08:46:30 -08:00
cache cache . FileCache
2020-07-24 08:42:11 -08:00
}
type streamJob struct {
ms * mediaStreamer
mf * model . MediaFile
format string
bitRate int
}
2020-10-24 11:10:27 -08:00
func ( j * streamJob ) Key ( ) string {
2020-07-28 13:16:01 -08:00
return fmt . Sprintf ( "%s.%s.%d.%s" , j . mf . ID , j . mf . UpdatedAt . Format ( time . RFC3339Nano ) , j . bitRate , j . format )
2020-02-03 07:54:59 -09:00
}
2020-03-16 10:28:13 -08:00
func ( ms * mediaStreamer ) NewStream ( ctx context . Context , id string , reqFormat string , reqBitRate int ) ( * Stream , error ) {
2020-02-24 09:31:05 -09:00
mf , err := ms . ds . MediaFile ( ctx ) . Get ( id )
2020-02-19 10:53:35 -09:00
if err != nil {
2020-02-24 09:31:05 -09:00
return nil , err
2020-02-19 10:53:35 -09:00
}
2020-02-03 07:54:59 -09:00
2022-12-18 08:12:37 -09:00
return ms . DoStream ( ctx , mf , reqFormat , reqBitRate )
}
func ( ms * mediaStreamer ) DoStream ( ctx context . Context , mf * model . MediaFile , reqFormat string , reqBitRate int ) ( * Stream , error ) {
2020-04-05 20:26:51 -08:00
var format string
var bitRate int
2020-05-21 17:26:48 -08:00
var cached bool
2020-04-05 20:26:51 -08:00
defer func ( ) {
2022-11-04 07:29:58 -08:00
log . Info ( ctx , "Streaming file" , "title" , mf . Title , "artist" , mf . Artist , "format" , format , "cached" , cached ,
2020-05-21 17:26:48 -08:00
"bitRate" , bitRate , "user" , userName ( ctx ) , "transcoding" , format != "raw" ,
"originalFormat" , mf . Suffix , "originalBitRate" , mf . BitRate )
2020-04-05 20:26:51 -08:00
} ( )
format , bitRate = selectTranscodingOptions ( ctx , ms . ds , mf , reqFormat , reqBitRate )
2020-02-24 09:31:05 -09:00
s := & Stream { ctx : ctx , mf : mf , format : format , bitRate : bitRate }
2020-02-19 10:53:35 -09:00
if format == "raw" {
2020-07-24 08:42:11 -08:00
log . Debug ( ctx , "Streaming RAW file" , "id" , mf . ID , "path" , mf . Path ,
2020-03-16 10:28:13 -08:00
"requestBitrate" , reqBitRate , "requestFormat" , reqFormat ,
2020-07-24 08:42:11 -08:00
"originalBitrate" , mf . BitRate , "originalFormat" , mf . Suffix ,
"selectedBitrate" , bitRate , "selectedFormat" , format )
2020-02-24 09:31:05 -09:00
f , err := os . Open ( mf . Path )
if err != nil {
return nil , err
}
2020-07-25 20:45:33 -08:00
s . ReadCloser = f
2020-02-24 09:31:05 -09:00
s . Seeker = f
s . format = mf . Suffix
return s , nil
2020-02-19 10:53:35 -09:00
}
2020-02-03 07:54:59 -09:00
2020-07-24 08:42:11 -08:00
job := & streamJob {
ms : ms ,
mf : mf ,
format : format ,
bitRate : bitRate ,
}
r , err := ms . cache . Get ( ctx , job )
2020-02-19 10:53:35 -09:00
if err != nil {
2020-07-24 09:30:27 -08:00
log . Error ( ctx , "Error accessing transcoding cache" , "id" , mf . ID , err )
2020-02-24 09:31:05 -09:00
return nil , err
2020-02-19 10:53:35 -09:00
}
2020-07-24 14:42:36 -08:00
cached = r . Cached
2020-02-20 15:08:10 -09:00
2020-11-05 14:11:12 -09:00
s . ReadCloser = r
s . Seeker = r . Seeker
2020-07-24 08:42:11 -08:00
log . Debug ( ctx , "Streaming TRANSCODED file" , "id" , mf . ID , "path" , mf . Path ,
2020-03-16 10:28:13 -08:00
"requestBitrate" , reqBitRate , "requestFormat" , reqFormat ,
2020-07-24 08:42:11 -08:00
"originalBitrate" , mf . BitRate , "originalFormat" , mf . Suffix ,
2020-07-25 20:45:33 -08:00
"selectedBitrate" , bitRate , "selectedFormat" , format , "cached" , cached , "seekable" , s . Seekable ( ) )
2020-07-24 08:42:11 -08:00
return s , nil
2020-02-03 07:54:59 -09:00
}
2020-02-24 09:31:05 -09:00
type Stream struct {
2020-02-20 15:08:10 -09:00
ctx context . Context
2020-02-19 10:53:35 -09:00
mf * model . MediaFile
bitRate int
2020-02-24 09:31:05 -09:00
format string
2020-07-25 20:45:33 -08:00
io . ReadCloser
2020-02-24 09:31:05 -09:00
io . Seeker
2020-02-03 07:54:59 -09:00
}
2020-02-24 09:31:05 -09:00
func ( s * Stream ) Seekable ( ) bool { return s . Seeker != nil }
func ( s * Stream ) Duration ( ) float32 { return s . mf . Duration }
func ( s * Stream ) ContentType ( ) string { return mime . TypeByExtension ( "." + s . format ) }
2020-04-02 20:24:40 -08:00
func ( s * Stream ) Name ( ) string { return s . mf . Title + "." + s . format }
2020-02-24 09:31:05 -09:00
func ( s * Stream ) ModTime ( ) time . Time { return s . mf . UpdatedAt }
2020-04-02 20:24:40 -08:00
func ( s * Stream ) EstimatedContentLength ( ) int {
return int ( s . mf . Duration * float32 ( s . bitRate ) / 8 * 1024 )
}
2020-02-09 18:09:18 -09:00
2020-03-16 10:28:13 -08:00
// TODO This function deserves some love (refactoring)
func selectTranscodingOptions ( ctx context . Context , ds model . DataStore , mf * model . MediaFile , reqFormat string , reqBitRate int ) ( format string , bitRate int ) {
format = "raw"
if reqFormat == "raw" {
return
}
2020-04-02 18:17:52 -08:00
if reqFormat == mf . Suffix && reqBitRate == 0 {
bitRate = mf . BitRate
return
}
2020-05-13 12:49:55 -08:00
trc , hasDefault := request . TranscodingFrom ( ctx )
2020-03-16 10:28:13 -08:00
var cFormat string
var cBitRate int
if reqFormat != "" {
cFormat = reqFormat
2020-02-24 09:31:05 -09:00
} else {
2020-03-16 10:28:13 -08:00
if hasDefault {
cFormat = trc . TargetFormat
cBitRate = trc . DefaultBitRate
2020-05-13 12:49:55 -08:00
if p , ok := request . PlayerFrom ( ctx ) ; ok {
2020-03-16 10:28:13 -08:00
cBitRate = p . MaxBitRate
}
2022-12-06 15:41:16 -09:00
} else if reqBitRate > 0 && conf . Server . DefaultDownsamplingFormat != "" {
// If no format is specified and no transcoding associated to the player, but a bitrate is specfied, and there is no transcoding set for the player, we use the default downsampling format
log . Debug ( "Default Downsampling" , "Using default downsampling format" , conf . Server . DefaultDownsamplingFormat )
cFormat = conf . Server . DefaultDownsamplingFormat
2020-02-24 09:31:05 -09:00
}
}
2020-03-16 10:28:13 -08:00
if reqBitRate > 0 {
cBitRate = reqBitRate
2020-02-24 09:31:05 -09:00
}
2020-03-16 10:28:13 -08:00
if cBitRate == 0 && cFormat == "" {
return
}
t , err := ds . Transcoding ( ctx ) . FindByFormat ( cFormat )
if err == nil {
format = t . TargetFormat
if cBitRate != 0 {
bitRate = cBitRate
} else {
bitRate = t . DefaultBitRate
}
}
2020-04-08 15:10:55 -08:00
if format == mf . Suffix && bitRate >= mf . BitRate {
2020-03-16 10:28:13 -08:00
format = "raw"
bitRate = 0
2020-02-24 09:31:05 -09:00
}
2022-11-30 10:16:30 -09:00
return format , bitRate
2020-02-03 07:54:59 -09:00
}
2020-10-27 13:02:20 -08:00
var (
onceTranscodingCache sync . Once
instanceTranscodingCache TranscodingCache
)
func GetTranscodingCache ( ) TranscodingCache {
onceTranscodingCache . Do ( func ( ) {
instanceTranscodingCache = cache . NewFileCache ( "Transcoding" , conf . Server . TranscodingCacheSize ,
consts . TranscodingCacheDir , consts . DefaultTranscodingCacheMaxItems ,
func ( ctx context . Context , arg cache . Item ) ( io . Reader , error ) {
job := arg . ( * streamJob )
t , err := job . ms . ds . Transcoding ( ctx ) . FindByFormat ( job . format )
if err != nil {
log . Error ( ctx , "Error loading transcoding command" , "format" , job . format , err )
return nil , os . ErrInvalid
}
2022-12-20 08:25:47 -09:00
out , err := job . ms . transcoder . Transcode ( ctx , t . Command , job . mf . Path , job . bitRate )
2020-10-27 13:02:20 -08:00
if err != nil {
log . Error ( ctx , "Error starting transcoder" , "id" , job . mf . ID , err )
return nil , os . ErrInvalid
}
return out , nil
} )
} )
return instanceTranscodingCache
2020-02-21 05:36:29 -09:00
}