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"
2023-09-24 17:13:06 +02:00
"path/filepath"
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
2023-09-22 23:59:56 +02:00
"github.com/photoprism/photoprism/internal/entity"
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-09-24 17:13:06 +02:00
"github.com/photoprism/photoprism/pkg/fs"
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
//
2024-01-08 14:53:39 +01:00
// The request parameters are:
//
// - hash: string The photo or video file hash as returned by the search API
// - type: string Video format
2022-08-10 16:09:21 +02:00
//
2024-01-07 12:25:56 +01:00
// GET /api/v1/videos/:hash/:token/:type
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
2023-09-24 17:13:06 +02:00
} else if f . FileHash == "" {
log . Errorf ( "video: file hash missing in index" )
c . Data ( http . StatusOK , "image/svg+xml" , videoIconSvg )
return
2020-05-13 15:36:42 +02:00
}
2023-09-24 17:13:06 +02:00
// Get app config.
2023-09-22 23:59:56 +02:00
conf := get . Config ( )
2023-09-24 17:13:06 +02:00
// Get video bitrate, codec, and file type.
videoBitrate := f . Bitrate ( )
videoCodec := f . FileCodec
videoFileType := f . FileType
videoFileName := photoprism . FileName ( f . FileRoot , f . FileName )
2023-09-22 23:59:56 +02:00
2023-09-23 11:27:20 +02:00
// If the file has a hybrid photo/video format, try to find and send the embedded video data.
if f . MediaType == entity . MediaLive {
2023-09-24 17:13:06 +02:00
if info , videoErr := video . ProbeFile ( videoFileName ) ; info . VideoOffset < 0 || ! info . Compatible || videoErr != nil {
2024-01-18 11:23:59 +01:00
logErr ( "video" , videoErr )
2023-09-24 17:13:06 +02:00
log . Warnf ( "video: no embedded media found in %s" , clean . Log ( f . FileName ) )
2023-09-22 23:59:56 +02:00
AddContentTypeHeader ( c , video . ContentTypeAVC )
c . File ( get . Config ( ) . StaticFile ( "video/404.mp4" ) )
2023-09-24 17:13:06 +02:00
return
} else if reader , readErr := video . NewReader ( videoFileName , info . VideoOffset ) ; readErr != nil {
log . Errorf ( "video: failed to read media embedded in %s (%s)" , clean . Log ( f . FileName ) , readErr )
2023-09-22 23:59:56 +02:00
AddContentTypeHeader ( c , video . ContentTypeAVC )
c . File ( get . Config ( ) . StaticFile ( "video/404.mp4" ) )
2023-09-24 17:13:06 +02:00
return
} else if c . Request . Header . Get ( "Range" ) == "" && info . VideoCodec == format . Codec {
2023-09-22 23:59:56 +02:00
defer reader . Close ( )
AddVideoCacheHeader ( c , conf . CdnVideo ( ) )
c . DataFromReader ( http . StatusOK , info . VideoSize ( ) , info . VideoContentType ( ) , reader , nil )
2023-09-24 17:13:06 +02:00
return
2024-01-21 14:22:16 +01:00
} else if cacheName , cacheErr := fs . CacheFileFromReader ( filepath . Join ( conf . MediaFileCachePath ( f . FileHash ) , f . FileHash + info . VideoFileExt ( ) ) , reader ) ; cacheErr != nil {
2023-09-24 17:13:06 +02:00
log . Errorf ( "video: failed to cache %s embedded in %s (%s)" , strings . ToUpper ( videoFileType ) , clean . Log ( f . FileName ) , cacheErr )
AddContentTypeHeader ( c , video . ContentTypeAVC )
c . File ( get . Config ( ) . StaticFile ( "video/404.mp4" ) )
return
} else {
// Serve embedded videos from cache to allow streaming and transcoding.
videoBitrate = info . VideoBitrate ( )
videoCodec = info . VideoCodec . String ( )
videoFileType = info . VideoFileType ( ) . String ( )
videoFileName = cacheName
log . Debugf ( "video: streaming %s encoded %s in %s from cache" , strings . ToUpper ( videoCodec ) , strings . ToUpper ( videoFileType ) , clean . Log ( f . FileName ) )
2023-09-22 23:59:56 +02:00
}
}
2023-09-24 17:13:06 +02:00
// Check video format support.
supported := videoCodec != "" && videoCodec == format . Codec . String ( ) || format . Codec == video . CodecUnknown && videoFileType == format . FileType . String ( )
2022-06-26 19:47:12 +02:00
2023-09-24 17:13:06 +02:00
// Check video bitrate against the configured limit.
transcode := ! supported || conf . FFmpegEnabled ( ) && conf . FFmpegBitrateExceeded ( videoBitrate )
2020-05-13 15:36:42 +02:00
2023-09-24 17:13:06 +02:00
if mediaFile , mediaErr := photoprism . NewMediaFile ( videoFileName ) ; mediaErr != nil {
2020-05-25 19:10:44 +02:00
// Set missing flag so that the file doesn't show up in search results anymore.
2024-01-18 11:23:59 +01:00
logErr ( "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 ) )
2023-09-24 17:13:06 +02:00
videoFileName = get . Config ( ) . StaticFile ( "video/404.mp4" )
2023-09-22 23:59:56 +02:00
AddContentTypeHeader ( c , video . ContentTypeAVC )
2022-06-26 19:47:12 +02:00
} else if transcode {
2023-09-24 17:13:06 +02:00
if videoCodec != "" {
log . Debugf ( "video: %s is %s encoded and cannot be streamed directly, average bitrate %.1f MBit/s" , clean . Log ( f . FileName ) , strings . ToUpper ( videoCodec ) , videoBitrate )
2022-06-24 06:59:22 +02:00
} else {
2023-09-24 17:13:06 +02:00
log . Debugf ( "video: %s cannot be streamed directly, average bitrate %.1f MBit/s" , clean . Log ( f . FileName ) , videoBitrate )
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-09-24 17:13:06 +02:00
if avcFile , avcErr := conv . ToAvc ( mediaFile , get . Config ( ) . FFmpegEncoder ( ) , false , false ) ; avcFile != nil && avcErr == nil {
videoFileName = avcFile . FileName ( )
2023-07-15 15:17:41 +02:00
} 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 ) )
2023-09-24 17:13:06 +02:00
videoFileName = get . Config ( ) . StaticFile ( "video/404.mp4" )
2020-12-05 04:24:10 +01:00
}
2020-05-13 15:36:42 +02:00
2023-09-22 23:59:56 +02:00
AddContentTypeHeader ( c , video . ContentTypeAVC )
2022-06-26 19:47:12 +02:00
} else {
2023-09-24 17:13:06 +02:00
if videoCodec != "" && videoCodec != videoFileType {
log . Debugf ( "video: %s is %s encoded and requires no transcoding, average bitrate %.1f MBit/s" , clean . Log ( f . FileName ) , strings . ToUpper ( videoCodec ) , videoBitrate )
AddContentTypeHeader ( c , fmt . Sprintf ( "%s; codecs=\"%s\"" , f . FileMime , clean . Codec ( videoCodec ) ) )
2022-06-26 19:47:12 +02:00
} else {
2023-09-24 17:13:06 +02:00
log . Debugf ( "video: %s is streamed directly, average bitrate %.1f MBit/s" , clean . Log ( f . FileName ) , videoBitrate )
2022-06-26 19:47:12 +02:00
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" ) != "" {
2023-09-24 17:13:06 +02:00
c . FileAttachment ( videoFileName , f . DownloadName ( DownloadName ( c ) , 0 ) )
2020-05-13 15:36:42 +02:00
} else {
2023-09-24 17:13:06 +02:00
c . File ( videoFileName )
2020-05-13 15:36:42 +02:00
}
return
} )
}