Videos: Improve MP4 AVC browser playback compatibility #1307 #3168

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-01-29 14:17:52 +01:00
parent a20f12f3e7
commit 790de0b146
11 changed files with 31 additions and 27 deletions

View File

@@ -17,6 +17,7 @@ export const Video = "video";
// - https://cconcolato.github.io/media-mime-support/ // - https://cconcolato.github.io/media-mime-support/
// - https://thorium.rocks/misc/h265-tester.html // - https://thorium.rocks/misc/h265-tester.html
export const CodecAVC = "avc1"; export const CodecAVC = "avc1";
export const CodecAVC3 = "avc3";
export const CodecHEVC = "hvc1"; export const CodecHEVC = "hvc1";
export const CodecHEV1 = "hev1"; export const CodecHEV1 = "hev1";
export const CodecVVC = "vvc1"; export const CodecVVC = "vvc1";
@@ -26,7 +27,6 @@ export const CodecVP8 = "vp08";
export const CodecVP9 = "vp09"; export const CodecVP9 = "vp09";
export const CodecAV1 = "av01"; export const CodecAV1 = "av01";
export const CodecAV1C = "av1c"; export const CodecAV1C = "av1c";
export const CodecAVC3 = "avc3";
// Media file formats. // Media file formats.
export const FormatMP4 = "mp4"; export const FormatMP4 = "mp4";
@@ -47,12 +47,13 @@ export const FormatSVG = "svg";
export const FormatGIF = "gif"; export const FormatGIF = "gif";
// HTTP Content types (MIME). // HTTP Content types (MIME).
export const ContentTypeAVC = 'video/mp4; codecs="avc1"'; export const ContentTypeMP4 = "video/mp4";
export const ContentTypeAVC = 'video/mp4; codecs="avc1.640028"';
export const ContentTypeHEVC = 'video/mp4; codecs="hvc1.2.4.L120.B0"'; export const ContentTypeHEVC = 'video/mp4; codecs="hvc1.2.4.L120.B0"';
export const ContentTypeHEV1 = 'video/mp4; codecs="hev1.2.4.L120.B0"'; export const ContentTypeHEV1 = 'video/mp4; codecs="hev1.2.4.L120.B0"';
export const ContentTypeVVC = 'video/mp4; codecs="vvc1"'; export const ContentTypeVVC = 'video/mp4; codecs="vvc1"';
export const ContentTypeOGV = "video/ogg"; export const ContentTypeOGV = "video/ogg";
export const ContentTypeWebM = "video/webm"; export const ContentTypeWebM = "video/webm";
export const ContentTypeVP8 = 'video/webm; codecs="vp08.02.41.10"'; export const ContentTypeVP8 = 'video/webm; codecs="vp8"';
export const ContentTypeVP9 = 'video/webm; codecs="vp09.00.50.08"'; export const ContentTypeVP9 = 'video/webm; codecs="vp09.00.10.08"';
export const ContentTypeAV1 = 'video/webm; codecs="av01.2.10M.10"'; export const ContentTypeAV1 = 'video/webm; codecs="av01.2.10M.10"';

View File

@@ -23,14 +23,14 @@ Additional information can be found in our Developer Guide:
*/ */
import * as media from "common/media";
import * as can from "common/can";
import { config } from "app/session"; import { config } from "app/session";
import { DATE_FULL } from "model/photo"; import { DATE_FULL } from "model/photo";
import sanitizeHtml from "sanitize-html"; import sanitizeHtml from "sanitize-html";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { $gettext } from "common/gettext"; import { $gettext } from "common/gettext";
import Notify from "common/notify"; import Notify from "common/notify";
import * as media from "common/media";
import * as can from "common/can";
const Nanosecond = 1; const Nanosecond = 1;
const Microsecond = 1000 * Nanosecond; const Microsecond = 1000 * Nanosecond;
@@ -405,9 +405,10 @@ export default class Util {
case "qt ": case "qt ":
return "Apple QuickTime (MOV)"; return "Apple QuickTime (MOV)";
case "avc": case "avc":
case "avc1": case media.CodecAVC:
case "avc3":
return "Advanced Video Coding (AVC) / H.264"; return "Advanced Video Coding (AVC) / H.264";
case media.CodecAVC3:
return "Advanced Video Coding (AVC) Bitstream";
case "hvc": case "hvc":
case "hev": case "hev":
case media.CodecHEVC: case media.CodecHEVC:
@@ -579,17 +580,17 @@ export default class Util {
) { ) {
return media.FormatVVC; return media.FormatVVC;
} else if (can.useOGV && (codec === media.CodecOGV || codec === media.FormatOGG || mime === media.ContentTypeOGV)) { } else if (can.useOGV && (codec === media.CodecOGV || codec === media.FormatOGG || mime === media.ContentTypeOGV)) {
return media.CodecOGV; return media.FormatOGG;
} else if ( } else if (
can.useVP8 && can.useVP8 &&
(codec === media.CodecVP8 || codec === media.FormatVP8 || mime?.startsWith('video/mp4; codecs="vp08')) (codec === media.CodecVP8 || codec === media.FormatVP8 || mime?.startsWith('video/mp4; codecs="vp08'))
) { ) {
return media.CodecVP8; return media.FormatVP8;
} else if ( } else if (
can.useVP9 && can.useVP9 &&
(codec === media.CodecVP9 || codec === media.FormatVP9 || mime?.startsWith('video/mp4; codecs="vp09')) (codec === media.CodecVP9 || codec === media.FormatVP9 || mime?.startsWith('video/mp4; codecs="vp09'))
) { ) {
return media.CodecVP9; return media.FormatVP9;
} else if ( } else if (
can.useAV1 && can.useAV1 &&
(codec === media.CodecAV1 || (codec === media.CodecAV1 ||

View File

@@ -267,6 +267,7 @@ nav .v-list__item__title.title {
padding-top: 12px; padding-top: 12px;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
scrollbar-width: none;
flex-grow: 1; flex-grow: 1;
} }

View File

@@ -587,9 +587,7 @@ export class Photo extends RestModel {
file = files.find((f) => f.MediaType === media.Image && f.Root === "/"); file = files.find((f) => f.MediaType === media.Image && f.Root === "/");
break; break;
case media.Live: case media.Live:
file = files.find( file = files.find((f) => (f.MediaType === media.Video || f.MediaType === media.Live) && f.Root === "/");
(f) => (f.MediaType === media.Video || f.MediaType === media.Live) && f.Root === "/"
);
break; break;
case media.Raw: case media.Raw:
case media.Video: case media.Video:

View File

@@ -30,7 +30,7 @@ describe("common/can", () => {
}); });
it("useVP8", () => { it("useVP8", () => {
assert.equal(can.useVP8, false); assert.equal(can.useVP8, true);
}); });
it("useVP9", () => { it("useVP9", () => {

View File

@@ -11,8 +11,8 @@ import (
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/authn" "github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/i18n" "github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/media/video"
) )
//go:embed embed/video.mp4 //go:embed embed/video.mp4
@@ -118,12 +118,12 @@ func AbortInvalidCredentials(c *gin.Context) {
func AbortVideo(c *gin.Context) { func AbortVideo(c *gin.Context) {
if c != nil { if c != nil {
c.Data(http.StatusOK, video.ContentTypeAVC, brokenVideo) c.Data(http.StatusOK, header.ContentTypeAVC, brokenVideo)
} }
} }
func AbortVideoWithStatus(c *gin.Context, code int) { func AbortVideoWithStatus(c *gin.Context, code int) {
if c != nil { if c != nil {
c.Data(code, video.ContentTypeAVC, brokenVideo) c.Data(code, header.ContentTypeAVC, brokenVideo)
} }
} }

View File

@@ -13,6 +13,7 @@ import (
"github.com/photoprism/photoprism/internal/photoprism/get" "github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/header"
"github.com/photoprism/photoprism/pkg/media/video" "github.com/photoprism/photoprism/pkg/media/video"
"github.com/photoprism/photoprism/pkg/rnd" "github.com/photoprism/photoprism/pkg/rnd"
) )
@@ -110,7 +111,7 @@ func GetVideo(router *gin.RouterGroup) {
} else if c.Request.Header.Get("Range") == "" && info.VideoCodec == format.Codec { } else if c.Request.Header.Get("Range") == "" && info.VideoCodec == format.Codec {
defer reader.Close() defer reader.Close()
AddVideoCacheHeader(c, conf.CdnVideo()) AddVideoCacheHeader(c, conf.CdnVideo())
c.DataFromReader(http.StatusOK, info.VideoSize(), info.VideoContentType(), reader, nil) c.DataFromReader(http.StatusOK, info.VideoSize(), clean.ContentType(info.VideoContentType()), reader, nil)
return return
} else if cacheName, cacheErr := fs.CacheFileFromReader(filepath.Join(conf.MediaFileCachePath(f.FileHash), f.FileHash+info.VideoFileExt()), reader); cacheErr != nil { } 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) log.Errorf("video: failed to cache %s embedded in %s (%s)", strings.ToUpper(videoFileType), clean.Log(f.FileName), cacheErr)
@@ -151,7 +152,7 @@ func GetVideo(router *gin.RouterGroup) {
if avcFile, avcErr := conv.ToAvc(mediaFile, get.Config().FFmpegEncoder(), false, false); avcFile != nil && avcErr == nil { if avcFile, avcErr := conv.ToAvc(mediaFile, get.Config().FFmpegEncoder(), false, false); avcFile != nil && avcErr == nil {
videoFileName = avcFile.FileName() videoFileName = avcFile.FileName()
AddContentTypeHeader(c, video.ContentTypeAVC) AddContentTypeHeader(c, header.ContentTypeAVC)
} else { } else {
// Log error and default to 404.mp4 // Log error and default to 404.mp4
log.Errorf("video: failed to transcode %s", clean.Log(f.FileName)) log.Errorf("video: failed to transcode %s", clean.Log(f.FileName))

View File

@@ -9,12 +9,14 @@ import (
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/media/video" "github.com/photoprism/photoprism/pkg/header"
) )
func TestGetVideo(t *testing.T) { func TestGetVideo(t *testing.T) {
t.Run("ContentTypeAVC", func(t *testing.T) { t.Run("ContentTypeAVC", func(t *testing.T) {
assert.Equal(t, video.ContentTypeAVC, fmt.Sprintf("%s; codecs=\"%s\"", "video/mp4", clean.Codec("avc1"))) assert.Equal(t, header.ContentTypeAVC, clean.ContentType("video/mp4; codecs=\"avc1\""))
mimeType := fmt.Sprintf("video/mp4; codecs=\"%s\"", clean.Codec("avc1"))
assert.Equal(t, header.ContentTypeAVC, clean.ContentType(mimeType))
}) })
t.Run("NoHash", func(t *testing.T) { t.Run("NoHash", func(t *testing.T) {

View File

@@ -158,7 +158,7 @@ func TestIsType(t *testing.T) {
assert.True(t, IsType("video/mp4", "video/mp4")) assert.True(t, IsType("video/mp4", "video/mp4"))
assert.True(t, IsType("video/mp4", MimeTypeMP4)) assert.True(t, IsType("video/mp4", MimeTypeMP4))
assert.True(t, IsType("video/mp4", "video/MP4")) assert.True(t, IsType("video/mp4", "video/MP4"))
assert.True(t, IsType("video/mp4", "video/MP4; codecs=\"avc1\"")) assert.True(t, IsType("video/mp4", "video/MP4; codecs=\"avc1.640028\""))
}) })
t.Run("False", func(t *testing.T) { t.Run("False", func(t *testing.T) {
assert.False(t, IsType("", MimeTypeMP4)) assert.False(t, IsType("", MimeTypeMP4))

View File

@@ -34,14 +34,14 @@ const (
ContentTypePNG = "image/png" ContentTypePNG = "image/png"
ContentTypeJPEG = "image/jpeg" ContentTypeJPEG = "image/jpeg"
ContentTypeSVG = "image/svg+xml" ContentTypeSVG = "image/svg+xml"
ContentTypeAVC = "video/mp4; codecs=\"avc1\"" // Advanced Video Coding (AVC), also known as H.264 ContentTypeAVC = "video/mp4; codecs=\"avc1.640028\"" // Advanced Video Coding (AVC), also known as H.264
ContentTypeHEVC = "video/mp4; codecs=\"hvc1.2.4.L120.B0\"" // HEVC MP4 Main10 Profile, Main Tier, Level 4.0 ContentTypeHEVC = "video/mp4; codecs=\"hvc1.2.4.L120.B0\"" // HEVC MP4 Main10 Profile, Main Tier, Level 4.0
ContentTypeHEV1 = "video/mp4; codecs=\"hev1.2.4.L120.B0\"" // HEVC Bitstream, not supported on macOS ContentTypeHEV1 = "video/mp4; codecs=\"hev1.2.4.L120.B0\"" // HEVC Bitstream, not supported on macOS
ContentTypeVVC = "video/mp4; codecs=\"vvc1\"" // Versatile Video Coding (VVC), also known as H.266 ContentTypeVVC = "video/mp4; codecs=\"vvc1\"" // Versatile Video Coding (VVC), also known as H.266
ContentTypeEVC = "video/mp4; codecs=\"evc1\"" // MPEG-5 Essential Video Coding (EVC), also known as ISO/IEC 23094-1 ContentTypeEVC = "video/mp4; codecs=\"evc1\"" // MPEG-5 Essential Video Coding (EVC), also known as ISO/IEC 23094-1
ContentTypeOGV = "video/ogg" ContentTypeOGV = "video/ogg"
ContentTypeWebM = "video/webm" ContentTypeWebM = "video/webm"
ContentTypeVP8 = "video/webm; codecs=\"vp08.02.41.10\"" ContentTypeVP8 = "video/webm; codecs=\"vp8\""
ContentTypeVP9 = "video/webm; codecs=\"vp09.00.50.08\"" ContentTypeVP9 = "video/webm; codecs=\"vp09.00.10.08\""
ContentTypeAV1 = "video/webm; codecs=\"av01.2.10M.10\"" ContentTypeAV1 = "video/webm; codecs=\"av01.2.10M.10\""
) )

View File

@@ -30,6 +30,6 @@ func TestContent(t *testing.T) {
assert.Equal(t, "image/png", ContentTypePNG) assert.Equal(t, "image/png", ContentTypePNG)
assert.Equal(t, "image/jpeg", ContentTypeJPEG) assert.Equal(t, "image/jpeg", ContentTypeJPEG)
assert.Equal(t, "image/svg+xml", ContentTypeSVG) assert.Equal(t, "image/svg+xml", ContentTypeSVG)
assert.Equal(t, "video/mp4; codecs=\"avc1\"", ContentTypeAVC) assert.Equal(t, "video/mp4; codecs=\"avc1.640028\"", ContentTypeAVC)
}) })
} }