mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -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"';
|
||||||
|
|||||||
@@ -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 ||
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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\""
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user