2020-07-10 18:45:58 +02:00
package core
2020-02-03 17:54:59 +01:00
import (
"context"
2020-02-19 20:53:35 +01:00
"fmt"
2020-02-03 17:54:59 +01:00
"io"
2020-02-24 19:31:05 +01:00
"mime"
2020-02-03 17:54:59 +01:00
"os"
2020-10-27 22:02:20 +01:00
"sync"
2020-02-03 17:54:59 +01:00
"time"
"github.com/navidrome/navidrome/conf"
2020-02-21 15:36:29 +01:00
"github.com/navidrome/navidrome/consts"
2020-07-10 18:45:58 +02:00
"github.com/navidrome/navidrome/core/transcoder"
2020-02-03 17:54:59 +01:00
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
2020-05-13 22:49:55 +02:00
"github.com/navidrome/navidrome/model/request"
2021-02-09 17:33:26 +01:00
"github.com/navidrome/navidrome/utils/cache"
2020-02-03 17:54:59 +01:00
)
type MediaStreamer interface {
2020-03-16 19:28:13 +01:00
NewStream ( ctx context . Context , id string , reqFormat string , reqBitRate int ) ( * Stream , error )
2022-12-18 18:12:37 +01:00
DoStream ( ctx context . Context , mf * model . MediaFile , reqFormat string , reqBitRate int ) ( * Stream , error )
2020-02-03 17:54:59 +01:00
}
2020-10-24 03:30:45 +02:00
type TranscodingCache cache . FileCache
2020-07-24 21:40:27 +02:00
2021-07-21 18:46:30 +02:00
func NewMediaStreamer ( ds model . DataStore , t transcoder . Transcoder , cache TranscodingCache ) MediaStreamer {
return & mediaStreamer { ds : ds , transcoder : t , cache : cache }
2020-02-03 17:54:59 +01:00
}
type mediaStreamer struct {
2021-07-21 18:46:30 +02:00
ds model . DataStore
transcoder transcoder . Transcoder
cache cache . FileCache
2020-07-24 18:42:11 +02:00
}
type streamJob struct {
ms * mediaStreamer
mf * model . MediaFile
format string
bitRate int
}
2020-10-24 21:10:27 +02:00
func ( j * streamJob ) Key ( ) string {
2020-07-28 23:16:01 +02:00
return fmt . Sprintf ( "%s.%s.%d.%s" , j . mf . ID , j . mf . UpdatedAt . Format ( time . RFC3339Nano ) , j . bitRate , j . format )
2020-02-03 17:54:59 +01:00
}
2020-03-16 19:28:13 +01:00
func ( ms * mediaStreamer ) NewStream ( ctx context . Context , id string , reqFormat string , reqBitRate int ) ( * Stream , error ) {
2020-02-24 19:31:05 +01:00
mf , err := ms . ds . MediaFile ( ctx ) . Get ( id )
2020-02-19 20:53:35 +01:00
if err != nil {
2020-02-24 19:31:05 +01:00
return nil , err
2020-02-19 20:53:35 +01:00
}
2020-02-03 17:54:59 +01:00
2022-12-18 18:12:37 +01: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-06 06:26:51 +02:00
var format string
var bitRate int
2020-05-22 03:26:48 +02:00
var cached bool
2020-04-06 06:26:51 +02:00
defer func ( ) {
2022-11-04 16:29:58 +01:00
log . Info ( ctx , "Streaming file" , "title" , mf . Title , "artist" , mf . Artist , "format" , format , "cached" , cached ,
2020-05-22 03:26:48 +02:00
"bitRate" , bitRate , "user" , userName ( ctx ) , "transcoding" , format != "raw" ,
"originalFormat" , mf . Suffix , "originalBitRate" , mf . BitRate )
2020-04-06 06:26:51 +02:00
} ( )
format , bitRate = selectTranscodingOptions ( ctx , ms . ds , mf , reqFormat , reqBitRate )
2020-02-24 19:31:05 +01:00
s := & Stream { ctx : ctx , mf : mf , format : format , bitRate : bitRate }
2020-02-19 20:53:35 +01:00
if format == "raw" {
2020-07-24 18:42:11 +02:00
log . Debug ( ctx , "Streaming RAW file" , "id" , mf . ID , "path" , mf . Path ,
2020-03-16 19:28:13 +01:00
"requestBitrate" , reqBitRate , "requestFormat" , reqFormat ,
2020-07-24 18:42:11 +02:00
"originalBitrate" , mf . BitRate , "originalFormat" , mf . Suffix ,
"selectedBitrate" , bitRate , "selectedFormat" , format )
2020-02-24 19:31:05 +01:00
f , err := os . Open ( mf . Path )
if err != nil {
return nil , err
}
2020-07-26 06:45:33 +02:00
s . ReadCloser = f
2020-02-24 19:31:05 +01:00
s . Seeker = f
s . format = mf . Suffix
return s , nil
2020-02-19 20:53:35 +01:00
}
2020-02-03 17:54:59 +01:00
2020-07-24 18:42:11 +02:00
job := & streamJob {
ms : ms ,
mf : mf ,
format : format ,
bitRate : bitRate ,
}
r , err := ms . cache . Get ( ctx , job )
2020-02-19 20:53:35 +01:00
if err != nil {
2020-07-24 19:30:27 +02:00
log . Error ( ctx , "Error accessing transcoding cache" , "id" , mf . ID , err )
2020-02-24 19:31:05 +01:00
return nil , err
2020-02-19 20:53:35 +01:00
}
2020-07-25 00:42:36 +02:00
cached = r . Cached
2020-02-21 01:08:10 +01:00
2020-11-06 00:11:12 +01:00
s . ReadCloser = r
s . Seeker = r . Seeker
2020-07-24 18:42:11 +02:00
log . Debug ( ctx , "Streaming TRANSCODED file" , "id" , mf . ID , "path" , mf . Path ,
2020-03-16 19:28:13 +01:00
"requestBitrate" , reqBitRate , "requestFormat" , reqFormat ,
2020-07-24 18:42:11 +02:00
"originalBitrate" , mf . BitRate , "originalFormat" , mf . Suffix ,
2020-07-26 06:45:33 +02:00
"selectedBitrate" , bitRate , "selectedFormat" , format , "cached" , cached , "seekable" , s . Seekable ( ) )
2020-07-24 18:42:11 +02:00
return s , nil
2020-02-03 17:54:59 +01:00
}
2020-02-24 19:31:05 +01:00
type Stream struct {
2020-02-21 01:08:10 +01:00
ctx context . Context
2020-02-19 20:53:35 +01:00
mf * model . MediaFile
bitRate int
2020-02-24 19:31:05 +01:00
format string
2020-07-26 06:45:33 +02:00
io . ReadCloser
2020-02-24 19:31:05 +01:00
io . Seeker
2020-02-03 17:54:59 +01:00
}
2020-02-24 19:31:05 +01: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-03 06:24:40 +02:00
func ( s * Stream ) Name ( ) string { return s . mf . Title + "." + s . format }
2020-02-24 19:31:05 +01:00
func ( s * Stream ) ModTime ( ) time . Time { return s . mf . UpdatedAt }
2020-04-03 06:24:40 +02:00
func ( s * Stream ) EstimatedContentLength ( ) int {
return int ( s . mf . Duration * float32 ( s . bitRate ) / 8 * 1024 )
}
2020-02-10 04:09:18 +01:00
2020-03-16 19:28:13 +01: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-03 04:17:52 +02:00
if reqFormat == mf . Suffix && reqBitRate == 0 {
bitRate = mf . BitRate
return
}
2020-05-13 22:49:55 +02:00
trc , hasDefault := request . TranscodingFrom ( ctx )
2020-03-16 19:28:13 +01:00
var cFormat string
var cBitRate int
if reqFormat != "" {
cFormat = reqFormat
2020-02-24 19:31:05 +01:00
} else {
2020-03-16 19:28:13 +01:00
if hasDefault {
cFormat = trc . TargetFormat
cBitRate = trc . DefaultBitRate
2020-05-13 22:49:55 +02:00
if p , ok := request . PlayerFrom ( ctx ) ; ok {
2020-03-16 19:28:13 +01:00
cBitRate = p . MaxBitRate
}
2022-12-07 01:41:16 +01: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 19:31:05 +01:00
}
}
2020-03-16 19:28:13 +01:00
if reqBitRate > 0 {
cBitRate = reqBitRate
2020-02-24 19:31:05 +01:00
}
2020-03-16 19:28:13 +01: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-09 01:10:55 +02:00
if format == mf . Suffix && bitRate >= mf . BitRate {
2020-03-16 19:28:13 +01:00
format = "raw"
bitRate = 0
2020-02-24 19:31:05 +01:00
}
2022-11-30 20:16:30 +01:00
return format , bitRate
2020-02-03 17:54:59 +01:00
}
2020-10-27 22:02:20 +01: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
}
2021-07-21 18:46:30 +02:00
out , err := job . ms . transcoder . Start ( ctx , t . Command , job . mf . Path , job . bitRate )
2020-10-27 22:02:20 +01: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 15:36:29 +01:00
}