parent
0b16a67c90
commit
846c635f22
7 changed files with 128 additions and 9 deletions
|
@ -249,7 +249,7 @@ export class Photo extends RestModel {
|
|||
return false;
|
||||
}
|
||||
|
||||
return this.Files.findIndex(f => f.Codec === CodecAvc1) !== -1 || this.Files.findIndex(f => f.Type === TypeMP4) !== -1;
|
||||
return this.Files.findIndex(f => f.Video) !== -1;
|
||||
}
|
||||
|
||||
videoFile() {
|
||||
|
|
|
@ -3,11 +3,12 @@ package api
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/video"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
|
@ -60,7 +61,7 @@ func GetVideo(router *gin.RouterGroup) {
|
|||
|
||||
fileName := photoprism.FileName(f.FileRoot, f.FileName)
|
||||
|
||||
if !fs.FileExists(fileName) {
|
||||
if mf, err := photoprism.NewMediaFile(fileName); err != nil {
|
||||
log.Errorf("video: file %s is missing", txt.Quote(f.FileName))
|
||||
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
|
||||
|
||||
|
@ -68,6 +69,18 @@ func GetVideo(router *gin.RouterGroup) {
|
|||
logError("video", f.Update("FileMissing", true))
|
||||
|
||||
return
|
||||
} else if !mf.IsPlayableVideo() {
|
||||
conv := service.Convert()
|
||||
avcFile, err := conv.ToAvc1(mf)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("video: failed transcoding %s", txt.Quote(f.FileName))
|
||||
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fileName = avcFile.FileName()
|
||||
}
|
||||
|
||||
if c.Query("download") != "" {
|
||||
|
|
|
@ -286,3 +286,79 @@ func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
|
|||
|
||||
return NewMediaFile(jpegName)
|
||||
}
|
||||
|
||||
// AvcConvertCommand returns the command for converting video files to AVC1.
|
||||
func (c *Convert) AvcConvertCommand(mf *MediaFile, avcName string) (result *exec.Cmd, useMutex bool, err error) {
|
||||
if mf.IsVideo() {
|
||||
result = exec.Command(c.conf.FFmpegBin(), "-i", mf.FileName(), avcName)
|
||||
} else {
|
||||
return nil, useMutex, fmt.Errorf("convert: file type %s not supported in %s", mf.FileType(), txt.Quote(mf.BaseName()))
|
||||
}
|
||||
|
||||
return result, useMutex, nil
|
||||
}
|
||||
|
||||
// ToAvc1 converts a single video file to AVC1 if possible.
|
||||
func (c *Convert) ToAvc1(video *MediaFile) (*MediaFile, error) {
|
||||
if !video.Exists() {
|
||||
return nil, fmt.Errorf("convert: can not convert to avc1, file does not exist (%s)", video.RelName(c.conf.OriginalsPath()))
|
||||
}
|
||||
|
||||
if video.IsPlayableVideo() {
|
||||
return video, nil
|
||||
}
|
||||
|
||||
avcName := fs.TypeMp4.FindFirst(video.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), c.conf.Settings().Index.Sequences)
|
||||
|
||||
mediaFile, err := NewMediaFile(avcName)
|
||||
|
||||
if err == nil && mediaFile.IsPlayableVideo() {
|
||||
return mediaFile, nil
|
||||
}
|
||||
|
||||
if !c.conf.SidecarWritable() {
|
||||
return nil, fmt.Errorf("convert: disabled in read only mode (%s)", video.RelName(c.conf.OriginalsPath()))
|
||||
}
|
||||
|
||||
avcName = fs.FileName(video.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.AvcExt, c.conf.Settings().Index.Sequences)
|
||||
fileName := video.RelName(c.conf.OriginalsPath())
|
||||
|
||||
log.Debugf("convert: %s -> %s", fileName, filepath.Base(avcName))
|
||||
|
||||
event.Publish("index.converting", event.Data{
|
||||
"fileType": video.FileType(),
|
||||
"fileName": fileName,
|
||||
"baseName": filepath.Base(fileName),
|
||||
"xmpName": "",
|
||||
})
|
||||
|
||||
cmd, useMutex, err := c.AvcConvertCommand(video, avcName)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if useMutex {
|
||||
// Make sure only one command is executed at a time.
|
||||
// See https://photo.stackexchange.com/questions/105969/darktable-cli-fails-because-of-locked-database-file
|
||||
c.cmdMutex.Lock()
|
||||
defer c.cmdMutex.Unlock()
|
||||
}
|
||||
|
||||
// Fetch command output.
|
||||
var out bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
// Run convert command.
|
||||
if err := cmd.Run(); err != nil {
|
||||
if stderr.String() != "" {
|
||||
return nil, errors.New(stderr.String())
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return NewMediaFile(avcName)
|
||||
}
|
||||
|
|
|
@ -663,7 +663,7 @@ func (m *MediaFile) IsBitmap() bool {
|
|||
|
||||
// IsVideo returns true if this is a video file.
|
||||
func (m *MediaFile) IsVideo() bool {
|
||||
return strings.HasPrefix(m.MimeType(), "video/")
|
||||
return strings.HasPrefix(m.MimeType(), "video/") || m.MediaType() == fs.MediaVideo
|
||||
}
|
||||
|
||||
// IsJson return true if this media file is a json sidecar file.
|
||||
|
@ -730,7 +730,7 @@ func (m *MediaFile) IsSidecar() bool {
|
|||
|
||||
// IsPlayableVideo returns true if this is a supported video file format.
|
||||
func (m *MediaFile) IsPlayableVideo() bool {
|
||||
return m.IsVideo() && m.HasFileType(fs.TypeMP4)
|
||||
return m.IsVideo() && m.HasFileType(fs.TypeMp4)
|
||||
}
|
||||
|
||||
// IsPhoto returns true if this file is a photo / image.
|
||||
|
|
|
@ -45,7 +45,7 @@ type Type struct {
|
|||
type TypeMap map[string]Type
|
||||
|
||||
var TypeMP4 = Type{
|
||||
Format: fs.TypeMP4,
|
||||
Format: fs.TypeMp4,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: true,
|
||||
|
|
|
@ -20,8 +20,17 @@ const (
|
|||
TypeRaw FileType = "raw" // RAW image file.
|
||||
TypeHEIF FileType = "heif" // High Efficiency Image File Format
|
||||
TypeMov FileType = "mov" // Video files.
|
||||
TypeMP4 FileType = "mp4"
|
||||
TypeMp4 FileType = "mp4"
|
||||
TypeHEVC FileType = "hevc"
|
||||
TypeAvi FileType = "avi"
|
||||
Type3gp FileType = "3gp"
|
||||
Type3g2 FileType = "3g2"
|
||||
TypeFlv FileType = "flv"
|
||||
TypeMkv FileType = "mkv"
|
||||
TypeMpg FileType = "mpg"
|
||||
TypeOgv FileType = "ogv"
|
||||
TypeWebm FileType = "webm"
|
||||
TypeWMV FileType = "wmv"
|
||||
TypeXMP FileType = "xmp" // Adobe XMP sidecar file (XML).
|
||||
TypeAAE FileType = "aae" // Apple sidecar file (XML).
|
||||
TypeXML FileType = "xml" // XML metadata / config / sidecar file.
|
||||
|
@ -39,6 +48,8 @@ type TypeExtensions map[FileType][]string
|
|||
const (
|
||||
YamlExt = ".yml"
|
||||
JpegExt = ".jpg"
|
||||
AvcExt = ".mp4"
|
||||
HevcExt = ".hevc"
|
||||
)
|
||||
|
||||
// FileExt contains the filename extensions of file formats known to PhotoPrism.
|
||||
|
@ -57,7 +68,17 @@ var FileExt = FileExtensions{
|
|||
".dng": TypeRaw,
|
||||
".mov": TypeMov,
|
||||
".avi": TypeAvi,
|
||||
".mp4": TypeMP4,
|
||||
".mp4": TypeMp4,
|
||||
".hevc": TypeHEVC,
|
||||
".3gp": Type3gp,
|
||||
".3g2": Type3g2,
|
||||
".flv": TypeFlv,
|
||||
".mkv": TypeMkv,
|
||||
".mpg": TypeMpg,
|
||||
".mpeg": TypeMpg,
|
||||
".ogv": TypeOgv,
|
||||
".webm": TypeWebm,
|
||||
".wmv": TypeWMV,
|
||||
".yml": TypeYaml,
|
||||
".yaml": TypeYaml,
|
||||
".jpg": TypeJpeg,
|
||||
|
|
|
@ -19,8 +19,17 @@ var MediaTypes = map[FileType]MediaType{
|
|||
TypeBitmap: MediaImage,
|
||||
TypeHEIF: MediaImage,
|
||||
TypeAvi: MediaVideo,
|
||||
TypeMP4: MediaVideo,
|
||||
TypeHEVC: MediaVideo,
|
||||
TypeMp4: MediaVideo,
|
||||
TypeMov: MediaVideo,
|
||||
Type3gp: MediaVideo,
|
||||
Type3g2: MediaVideo,
|
||||
TypeFlv: MediaVideo,
|
||||
TypeMkv: MediaVideo,
|
||||
TypeMpg: MediaVideo,
|
||||
TypeOgv: MediaVideo,
|
||||
TypeWebm: MediaVideo,
|
||||
TypeWMV: MediaVideo,
|
||||
TypeXMP: MediaSidecar,
|
||||
TypeXML: MediaSidecar,
|
||||
TypeAAE: MediaSidecar,
|
||||
|
|
Loading…
Reference in a new issue