2020-05-13 15:36:42 +02:00
package api
import (
2022-06-24 06:59:22 +02:00
"fmt"
2020-05-13 15:36:42 +02:00
"net/http"
2022-06-26 19:47:12 +02:00
"strings"
2020-05-13 15:36:42 +02:00
"github.com/gin-gonic/gin"
2022-04-06 17:46:41 +02:00
2022-10-15 21:54:11 +02:00
"github.com/photoprism/photoprism/internal/get"
2020-06-07 10:09:35 +02:00
"github.com/photoprism/photoprism/internal/photoprism"
2020-05-13 15:36:42 +02:00
"github.com/photoprism/photoprism/internal/query"
2022-04-15 09:42:07 +02:00
"github.com/photoprism/photoprism/pkg/clean"
2023-02-14 14:43:49 +01:00
"github.com/photoprism/photoprism/pkg/video"
2020-05-13 15:36:42 +02:00
)
2023-08-15 11:06:43 +02:00
// GetVideo streams video content.
2022-01-18 12:28:27 +01:00
//
2020-05-27 19:38:40 +02:00
// GET /api/v1/videos/:hash/:token/:type
2020-05-13 15:36:42 +02:00
//
// Parameters:
2022-08-10 16:09:21 +02:00
//
// hash: string The photo or video file hash as returned by the search API
// type: string Video format
2020-06-25 14:54:04 +02:00
func GetVideo ( router * gin . RouterGroup ) {
2022-04-12 13:28:28 +02:00
router . GET ( "/videos/:hash/:token/:format" , func ( c * gin . Context ) {
2020-06-26 16:11:56 +02:00
if InvalidPreviewToken ( c ) {
2020-05-27 19:38:40 +02:00
c . Data ( http . StatusForbidden , "image/svg+xml" , brokenIconSvg )
return
}
2022-04-15 09:42:07 +02:00
fileHash := clean . Token ( c . Param ( "hash" ) )
formatName := clean . Token ( c . Param ( "format" ) )
2020-05-13 15:36:42 +02:00
2022-04-15 09:42:07 +02:00
format , ok := video . Types [ formatName ]
2020-05-13 15:36:42 +02:00
if ! ok {
2022-04-15 09:42:07 +02:00
log . Errorf ( "video: invalid format %s" , clean . Log ( formatName ) )
2020-05-13 15:36:42 +02:00
c . Data ( http . StatusOK , "image/svg+xml" , videoIconSvg )
return
}
f , err := query . FileByHash ( fileHash )
if err != nil {
2023-02-22 21:38:25 +01:00
log . Errorf ( "video: requested file not found (%s)" , err )
2020-05-13 15:36:42 +02:00
c . Data ( http . StatusOK , "image/svg+xml" , videoIconSvg )
return
}
2020-05-20 10:42:48 +02:00
if ! f . FileVideo {
2020-05-23 20:58:58 +02:00
f , err = query . VideoByPhotoUID ( f . PhotoUID )
2020-05-20 10:42:48 +02:00
if err != nil {
2023-02-22 21:38:25 +01:00
log . Errorf ( "video: no playable file found (%s)" , err )
2020-05-20 10:42:48 +02:00
c . Data ( http . StatusOK , "image/svg+xml" , videoIconSvg )
return
}
}
2020-05-13 15:36:42 +02:00
if f . FileError != "" {
2023-02-22 16:33:33 +01:00
log . Errorf ( "video: file has error %s" , f . FileError )
2020-05-13 15:36:42 +02:00
c . Data ( http . StatusOK , "image/svg+xml" , videoIconSvg )
return
}
2020-06-07 10:09:35 +02:00
fileName := photoprism . FileName ( f . FileRoot , f . FileName )
2022-06-26 19:47:12 +02:00
fileBitrate := f . Bitrate ( )
// File format supported by the client/browser?
supported := f . FileCodec != "" && f . FileCodec == string ( format . Codec ) || format . Codec == video . UnknownCodec && f . FileType == string ( format . File )
// File bitrate too high (for streaming)?
2022-10-15 21:54:11 +02:00
conf := get . Config ( )
2022-06-26 19:47:12 +02:00
transcode := ! supported || conf . FFmpegEnabled ( ) && conf . FFmpegBitrateExceeded ( fileBitrate )
2020-05-13 15:36:42 +02:00
2020-12-05 04:24:10 +01:00
if mf , err := photoprism . NewMediaFile ( fileName ) ; err != nil {
2020-05-25 19:10:44 +02:00
// Set missing flag so that the file doesn't show up in search results anymore.
2020-05-28 16:26:22 +02:00
logError ( "video" , f . Update ( "FileMissing" , true ) )
2020-05-25 19:10:44 +02:00
2022-06-24 06:59:22 +02:00
// Log error and default to 404.mp4
log . Errorf ( "video: file %s is missing" , clean . Log ( f . FileName ) )
2022-10-15 21:54:11 +02:00
fileName = get . Config ( ) . StaticFile ( "video/404.mp4" )
2022-06-24 06:59:22 +02:00
AddContentTypeHeader ( c , ContentTypeAvc )
2022-06-26 19:47:12 +02:00
} else if transcode {
if f . FileCodec != "" {
log . Debugf ( "video: %s is %s compressed and cannot be streamed directly, average bitrate %.1f MBit/s" , clean . Log ( f . FileName ) , clean . Log ( strings . ToUpper ( f . FileCodec ) ) , fileBitrate )
2022-06-24 06:59:22 +02:00
} else {
2022-06-26 19:47:12 +02:00
log . Debugf ( "video: %s cannot be streamed directly, average bitrate %.1f MBit/s" , clean . Log ( f . FileName ) , fileBitrate )
2022-06-24 06:59:22 +02:00
}
2022-06-26 19:47:12 +02:00
2022-10-15 21:54:11 +02:00
conv := get . Convert ( )
2020-12-05 04:24:10 +01:00
2023-07-15 15:17:41 +02:00
if avcFile , avcErr := conv . ToAvc ( mf , get . Config ( ) . FFmpegEncoder ( ) , false , false ) ; avcFile != nil && avcErr == nil {
fileName = avcFile . FileName ( )
} else {
2022-06-24 06:59:22 +02:00
// Log error and default to 404.mp4
2023-07-15 15:17:41 +02:00
log . Errorf ( "video: failed to transcode %s" , clean . Log ( f . FileName ) )
2022-10-15 21:54:11 +02:00
fileName = get . Config ( ) . StaticFile ( "video/404.mp4" )
2020-12-05 04:24:10 +01:00
}
2020-05-13 15:36:42 +02:00
2022-06-24 01:30:48 +02:00
AddContentTypeHeader ( c , ContentTypeAvc )
2022-06-26 19:47:12 +02:00
} else {
if f . FileCodec != "" && f . FileCodec != f . FileType {
log . Debugf ( "video: %s is %s compressed and requires no transcoding, average bitrate %.1f MBit/s" , clean . Log ( f . FileName ) , clean . Log ( strings . ToUpper ( f . FileCodec ) ) , fileBitrate )
AddContentTypeHeader ( c , fmt . Sprintf ( "%s; codecs=\"%s\"" , f . FileMime , clean . Codec ( f . FileCodec ) ) )
} else {
log . Debugf ( "video: %s is streamed directly, average bitrate %.1f MBit/s" , clean . Log ( f . FileName ) , fileBitrate )
AddContentTypeHeader ( c , f . FileMime )
}
2022-06-24 01:30:48 +02:00
}
2020-12-12 17:53:19 +01:00
2023-04-14 14:46:56 +02:00
// Add HTTP cache header.
2023-08-15 11:06:43 +02:00
AddVideoCacheHeader ( c , conf . CdnVideo ( ) )
2023-04-14 14:46:56 +02:00
// Return requested content.
2020-05-13 15:36:42 +02:00
if c . Query ( "download" ) != "" {
2021-01-27 21:30:10 +01:00
c . FileAttachment ( fileName , f . DownloadName ( DownloadName ( c ) , 0 ) )
2020-05-13 15:36:42 +02:00
} else {
c . File ( fileName )
}
return
} )
}