Videos: Improve downloading, remuxing, and transcoding #4982 #4892 #5040

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-06-09 15:31:23 +02:00
parent a45d9d30b9
commit 2e2ebab433
67 changed files with 936 additions and 258 deletions

View File

@@ -155,8 +155,11 @@ var Extensions = FileExtensions{
".flv": VideoFlash,
".f4v": VideoFlash,
".mkv": VideoMkv,
".ts": VideoM2TS,
".m2t": VideoM2TS,
".m2ts": VideoM2TS,
".mp2t": VideoM2TS,
".mts": VideoAvcHD,
".m2ts": VideoBDAV,
".ogv": VideoTheora,
".ogg": VideoTheora,
".ogx": VideoTheora,

View File

@@ -51,8 +51,8 @@ var TypeInfo = TypeMap{
VideoMkv: "Matroska Multimedia Container",
VideoMpeg: "Moving Picture Experts Group (MPEG)",
VideoMjpeg: "Motion JPEG",
VideoM2TS: "MPEG-2 Transport Stream (M2TS)",
VideoAvcHD: "Advanced Video Coding High Definition (AVCHD)",
VideoBDAV: "Blu-ray MPEG-2 Transport Stream",
VideoTheora: "Ogg Media (OGG)",
SidecarXMP: "Adobe Extensible Metadata Platform",
SidecarAppleXml: "Apple Image Edits XML",

View File

@@ -87,8 +87,8 @@ const (
Video3GP Type = "3gp" // Mobile Multimedia Container, MPEG-4 Part 12
Video3G2 Type = "3g2" // Similar to 3GP, consumes less space & bandwidth
VideoFlash Type = "flv" // Flash Video
VideoM2TS Type = "m2t" // MPEG-2 Transport Stream (M2TS)
VideoAvcHD Type = "mts" // AVCHD (Advanced Video Coding High Definition)
VideoBDAV Type = "m2ts" // Blu-ray MPEG-2 Transport Stream
VideoTheora Type = "ogv" // Ogg container format maintained by the Xiph.Org, free and open
VideoASF Type = "asf" // Advanced Systems/Streaming Format (ASF)
VideoAVI Type = "avi" // Microsoft Audio Video Interleave (AVI)

View File

@@ -29,6 +29,9 @@ func MimeType(filename string) (mimeType string) {
// Determine mime type based on the extension for the following
// formats, which otherwise cannot be reliably distinguished:
switch fileType {
// MPEG-2 Transport Stream
case VideoM2TS, VideoAvcHD:
return header.ContentTypeM2TS
// Apple QuickTime Container
case VideoMov:
return header.ContentTypeMov

View File

@@ -56,8 +56,8 @@ var Formats = map[fs.Type]Type{
fs.Video3GP: Video,
fs.Video3G2: Video,
fs.VideoFlash: Video,
fs.VideoM2TS: Video,
fs.VideoAvcHD: Video,
fs.VideoBDAV: Video,
fs.VideoTheora: Video,
fs.VideoASF: Video,
fs.VideoWMV: Video,

View File

@@ -23,6 +23,7 @@ import (
// Standard ContentType strings for audio and video files:
const (
ContentTypeM2TS = "video/mp2t"
ContentTypeM4v = "video/x-m4v"
ContentTypeMp4 = "video/mp4"
ContentTypeMp4Avc = ContentTypeMp4 + "; codecs=\"avc1\"" // MPEG-4 AVC (H.264)

View File

@@ -76,7 +76,6 @@ var CompatibleBrands = Chunks{
ChunkHEV2,
ChunkHEV3,
ChunkDVHE,
ChunkHEIC,
ChunkAV01,
ChunkAV1C,
ChunkMMP4,

View File

@@ -60,13 +60,13 @@ func (c Chunk) FileOffset(fileName string) (int, error) {
defer file.Close()
index, err := c.DataOffset(file, -1)
index, err := c.DataOffset(file, 0, -1)
return index, err
}
// DataOffset returns the index of the chunk in file, or -1 if it was not found.
func (c Chunk) DataOffset(file io.ReadSeeker, maxOffset int) (int, error) {
func (c Chunk) DataOffset(file io.ReadSeeker, offset, maxOffset int) (int, error) {
if file == nil {
return -1, errors.New("file is nil")
}
@@ -79,8 +79,11 @@ func (c Chunk) DataOffset(file io.ReadSeeker, maxOffset int) (int, error) {
// Create buffered read seeker.
r := bufseekio.NewReadSeeker(file, blockSize, dataSize)
// Index offset.
var offset int
if seekOffset, seekErr := r.Seek(int64(offset), io.SeekStart); seekErr != nil {
return -1, seekErr
} else {
offset = int(seekOffset)
}
// Search in batches.
for {

View File

@@ -39,6 +39,17 @@ func TestChunk_FileOffset(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 23213, index)
})
t.Run("motion-photo.heif", func(t *testing.T) {
index, err := ChunkFTYP.FileOffset("testdata/motion-photo.heif")
require.NoError(t, err)
assert.Equal(t, 4, index)
index, err = ChunkHEIC.FileOffset("testdata/motion-photo.heif")
require.NoError(t, err)
assert.Equal(t, 8, index)
index, err = ChunkHVC1.FileOffset("testdata/motion-photo.heif")
require.NoError(t, err)
assert.Equal(t, 976016, index)
})
}
func TestChunks(t *testing.T) {

View File

@@ -33,6 +33,7 @@ const (
CodecVp08 Codec = "vp08" // Google VP8
CodecVp09 Codec = "vp09" // Google VP9
CodecTheora Codec = "ogv" // Ogg Vorbis Video
CodecM2TS Codec = "m2t" // MPEG-2 Transport Stream
CodecWebm Codec = "webm" // Google WebM
)

View File

@@ -64,6 +64,8 @@ func ContentType(mediaType, fileType, videoCodec string, hdr bool) string {
mediaType = header.ContentTypeMp4
case fs.VideoMkv.Equal(fileType):
mediaType = header.ContentTypeMkv
case fs.VideoM2TS.Equal(fileType) || videoCodec == CodecM2TS:
mediaType = header.ContentTypeM2TS
}
}

View File

@@ -153,8 +153,7 @@ func Probe(file io.ReadSeeker) (info Info, err error) {
// If no AVC video was found, search the video data for High Efficiency Video Coding (HEVC) chunks,
// see https://stackoverflow.com/questions/63468587/what-hevc-codec-tag-to-use-with-fmp4-hvc1-or-hev1.
if info.VideoCodec == "" {
// To improve performance, only search for "hvc1" as that is the most common HEVC video identifier.
if fileOffset, fileErr := ChunkHVC1.DataOffset(file, -1); fileOffset > 0 && fileErr == nil {
if fileOffset, fileErr := ChunkHVC1.DataOffset(file, 0, -1); fileOffset > 0 && fileErr == nil {
info.VideoCodec = CodecHvc1
}
}

View File

@@ -252,4 +252,34 @@ func TestProbe(t *testing.T) {
assert.Equal(t, false, info.FastStart)
assert.Equal(t, true, info.Compatible)
})
t.Run("motion-photo.heif", func(t *testing.T) {
f, fileErr := os.Open("testdata/motion-photo.heif")
require.NoError(t, fileErr)
defer f.Close()
info, err := Probe(f)
require.NoError(t, err)
require.NotNil(t, info)
assert.Equal(t, "", info.FileName)
assert.Equal(t, int64(-1), info.FileSize)
assert.Equal(t, fs.TypeUnknown, info.FileType)
assert.Equal(t, Mp4, info.VideoType)
assert.Equal(t, int64(978741), info.VideoOffset)
assert.Equal(t, int64(0), info.ThumbOffset)
assert.Equal(t, media.Live, info.MediaType)
assert.Equal(t, CodecHvc1, info.VideoCodec)
assert.Equal(t, header.ContentTypeMp4, info.VideoMimeType)
assert.Equal(t, header.ContentTypeMp4HvcMain10, info.VideoContentType())
assert.Equal(t, "2.9686s", info.Duration.String())
assert.InEpsilon(t, 2.9686, info.Duration.Seconds(), 0.01)
assert.Equal(t, 2, info.Tracks)
assert.Equal(t, 0, info.VideoWidth)
assert.Equal(t, 0, info.VideoHeight)
assert.Equal(t, 89, info.Frames)
assert.Equal(t, 30.0, info.FPS)
assert.Equal(t, false, info.Encrypted)
assert.Equal(t, false, info.FastStart)
assert.Equal(t, true, info.Compatible)
})
}

View File

@@ -74,6 +74,9 @@ var Types = Standards{
"mkv1": MkvAv1,
"ogg": Theora, // ↓ Theora video in OGG container
"ogv": Theora,
"m2t": M2TS, // ↓ MPEG-2 Transport Stream container
"m2ts": M2TS,
"mp2t": M2TS,
"mp4": Mp4, // ↓ Unknown codec in MP4 container
"mpeg4": Mp4,
"webm": Webm, // ↓ Unknown codec in WebM container

BIN
pkg/media/video/testdata/bear.m2ts vendored Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -21,6 +21,16 @@ var Mp4 = Type{
Public: true,
}
// M2TS specifies the MPEG-2 Transport Stream (M2TS) multimedia container format.
var M2TS = Type{
Codec: CodecAvc1,
FileType: fs.VideoM2TS,
ContentType: header.ContentTypeM2TS,
WidthLimit: 8192,
HeightLimit: 4320,
Public: false,
}
// Mov specifies the Apple QuickTime (QT) container format.
var Mov = Type{
Codec: CodecAvc1,