/
video.go
170 lines (145 loc) Β· 6.17 KB
/
video.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package api
import (
"fmt"
"net/http"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/get"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/video"
)
// GetVideo streams video content.
//
// The request parameters are:
//
// - hash: string The photo or video file hash as returned by the search API
// - type: string Video format
//
// GET /api/v1/videos/:hash/:token/:type
func GetVideo(router *gin.RouterGroup) {
router.GET("/videos/:hash/:token/:format", func(c *gin.Context) {
if InvalidPreviewToken(c) {
c.Data(http.StatusForbidden, "image/svg+xml", brokenIconSvg)
return
}
fileHash := clean.Token(c.Param("hash"))
formatName := clean.Token(c.Param("format"))
format, ok := video.Types[formatName]
if !ok {
log.Errorf("video: invalid format %s", clean.Log(formatName))
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
return
}
f, err := query.FileByHash(fileHash)
if err != nil {
log.Errorf("video: requested file not found (%s)", err)
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
return
}
if !f.FileVideo {
f, err = query.VideoByPhotoUID(f.PhotoUID)
if err != nil {
log.Errorf("video: no playable file found (%s)", err)
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
return
}
}
if f.FileError != "" {
log.Errorf("video: file has error %s", f.FileError)
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
return
} else if f.FileHash == "" {
log.Errorf("video: file hash missing in index")
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
return
}
// Get app config.
conf := get.Config()
// Get video bitrate, codec, and file type.
videoBitrate := f.Bitrate()
videoCodec := f.FileCodec
videoFileType := f.FileType
videoFileName := photoprism.FileName(f.FileRoot, f.FileName)
// If the file has a hybrid photo/video format, try to find and send the embedded video data.
if f.MediaType == entity.MediaLive {
if info, videoErr := video.ProbeFile(videoFileName); info.VideoOffset < 0 || !info.Compatible || videoErr != nil {
logErr("video", videoErr)
log.Warnf("video: no embedded media found in %s", clean.Log(f.FileName))
AddContentTypeHeader(c, video.ContentTypeAVC)
c.File(get.Config().StaticFile("video/404.mp4"))
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)
AddContentTypeHeader(c, video.ContentTypeAVC)
c.File(get.Config().StaticFile("video/404.mp4"))
return
} else if c.Request.Header.Get("Range") == "" && info.VideoCodec == format.Codec {
defer reader.Close()
AddVideoCacheHeader(c, conf.CdnVideo())
c.DataFromReader(http.StatusOK, info.VideoSize(), info.VideoContentType(), reader, nil)
return
} else if cacheName, cacheErr := fs.CacheFileFromReader(filepath.Join(conf.MediaFileCachePath(f.FileHash), f.FileHash+info.VideoFileExt()), reader); cacheErr != nil {
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))
}
}
// Check video format support.
supported := videoCodec != "" && videoCodec == format.Codec.String() || format.Codec == video.CodecUnknown && videoFileType == format.FileType.String()
// Check video bitrate against the configured limit.
transcode := !supported || conf.FFmpegEnabled() && conf.FFmpegBitrateExceeded(videoBitrate)
if mediaFile, mediaErr := photoprism.NewMediaFile(videoFileName); mediaErr != nil {
// Set missing flag so that the file doesn't show up in search results anymore.
logErr("video", f.Update("FileMissing", true))
// Log error and default to 404.mp4
log.Errorf("video: file %s is missing", clean.Log(f.FileName))
videoFileName = get.Config().StaticFile("video/404.mp4")
AddContentTypeHeader(c, video.ContentTypeAVC)
} else if transcode {
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)
} else {
log.Debugf("video: %s cannot be streamed directly, average bitrate %.1f MBit/s", clean.Log(f.FileName), videoBitrate)
}
conv := get.Convert()
if avcFile, avcErr := conv.ToAvc(mediaFile, get.Config().FFmpegEncoder(), false, false); avcFile != nil && avcErr == nil {
videoFileName = avcFile.FileName()
} else {
// Log error and default to 404.mp4
log.Errorf("video: failed to transcode %s", clean.Log(f.FileName))
videoFileName = get.Config().StaticFile("video/404.mp4")
}
AddContentTypeHeader(c, video.ContentTypeAVC)
} else {
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)))
} else {
log.Debugf("video: %s is streamed directly, average bitrate %.1f MBit/s", clean.Log(f.FileName), videoBitrate)
AddContentTypeHeader(c, f.FileMime)
}
}
// Add HTTP cache header.
AddVideoCacheHeader(c, conf.CdnVideo())
// Return requested content.
if c.Query("download") != "" {
c.FileAttachment(videoFileName, f.DownloadName(DownloadName(c), 0))
} else {
c.File(videoFileName)
}
return
})
}