Videos: Stream OGV, VP8, VP9, AV1, WebM, and HEVC if supported #2461
This commit is contained in:
parent
95c03afe28
commit
519f0c49c9
24 changed files with 547 additions and 44 deletions
BIN
assets/static/video/404.mp4
Normal file
BIN
assets/static/video/404.mp4
Normal file
Binary file not shown.
47
frontend/src/common/caniuse.js
Normal file
47
frontend/src/common/caniuse.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
|
||||
Copyright (c) 2018 - 2022 PhotoPrism UG. All rights reserved.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||
<https://docs.photoprism.app/license/agpl>
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||
which describe how our Brand Assets may be used:
|
||||
<https://photoprism.app/trademark>
|
||||
|
||||
Feel free to send an email to hello@photoprism.app if you have questions,
|
||||
want to support our work, or just want to say hello.
|
||||
|
||||
Additional information can be found in our Developer Guide:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
|
||||
*/
|
||||
|
||||
export const canUseVideo = !!document.createElement("video").canPlayType;
|
||||
export const canUseAvc = canUseVideo
|
||||
? !!document.createElement("video").canPlayType('video/mp4; codecs="avc1"')
|
||||
: false;
|
||||
export const canUseOGV = canUseVideo // Ogg Theora
|
||||
? !!document.createElement("video").canPlayType("video/ogg")
|
||||
: false;
|
||||
export const canUseVP8 = canUseVideo // WebM VP8
|
||||
? !!document.createElement("video").canPlayType('video/webm; codecs="vp8"')
|
||||
: false;
|
||||
export const canUseVP9 = canUseVideo // WebM VP9
|
||||
? !!document.createElement("video").canPlayType('video/webm; codecs="vp9"')
|
||||
: false;
|
||||
export const canUseAv1 = canUseVideo // AV1, Main Profile, Level 4.0 Main Tier, 8-bit
|
||||
? !!document.createElement("video").canPlayType('video/webm; codecs="av01.0.08M.08"')
|
||||
: false;
|
||||
export const canUseWebm = canUseVideo
|
||||
? !!document.createElement("video").canPlayType("video/webm")
|
||||
: false;
|
||||
export const canUseHevc = canUseVideo
|
||||
? !!document.createElement("video").canPlayType('video/mp4; codecs="hvc1"')
|
||||
: false;
|
|
@ -37,12 +37,19 @@ import { $gettext } from "common/vm";
|
|||
import Clipboard from "common/clipboard";
|
||||
import download from "common/download";
|
||||
import * as src from "common/src";
|
||||
import { canUseOGV, canUseVP8, canUseVP9, canUseAv1, canUseWebm, canUseHevc } from "common/caniuse";
|
||||
|
||||
export const CodecOGV = "ogv";
|
||||
export const CodecVP8 = "vp8";
|
||||
export const CodecVP9 = "vp9";
|
||||
export const CodecAv1 = "av01";
|
||||
export const CodecAvc1 = "avc1";
|
||||
export const CodecHvc1 = "hvc1";
|
||||
export const FormatMp4 = "mp4";
|
||||
export const FormatAv1 = "av01";
|
||||
export const FormatAvc = "avc";
|
||||
export const FormatHvc = "hvc";
|
||||
export const FormatHevc = "hevc";
|
||||
export const FormatWebM = "webm";
|
||||
export const FormatGif = "gif";
|
||||
export const FormatJpeg = "jpg";
|
||||
export const MediaImage = "image";
|
||||
|
@ -476,14 +483,25 @@ export class Photo extends RestModel {
|
|||
|
||||
videoUrl() {
|
||||
let file = this.videoFile();
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
|
||||
if (file && file.Codec === CodecHvc1 && isSafari) {
|
||||
return `${config.apiUri}/videos/${file.Hash}/${config.previewToken()}/${FormatHvc}`;
|
||||
}
|
||||
|
||||
if (file) {
|
||||
return `${config.apiUri}/videos/${file.Hash}/${config.previewToken()}/${FormatAvc}`;
|
||||
let videoFormat = FormatAvc;
|
||||
|
||||
if (canUseHevc && file.Codec === CodecHvc1) {
|
||||
videoFormat = FormatHevc;
|
||||
} else if (canUseOGV && file.Codec === CodecOGV) {
|
||||
videoFormat = CodecOGV;
|
||||
} else if (canUseVP8 && file.Codec === CodecVP8) {
|
||||
videoFormat = CodecVP8;
|
||||
} else if (canUseVP9 && file.Codec === CodecVP9) {
|
||||
videoFormat = CodecVP9;
|
||||
} else if (canUseAv1 && file.Codec === CodecAv1) {
|
||||
videoFormat = FormatAv1;
|
||||
} else if (canUseWebm && file.FileType === FormatWebM) {
|
||||
videoFormat = FormatWebM;
|
||||
}
|
||||
|
||||
return `${config.apiUri}/videos/${file.Hash}/${config.previewToken()}/${videoFormat}`;
|
||||
}
|
||||
|
||||
return `${config.apiUri}/videos/${this.Hash}/${config.previewToken()}/${FormatAvc}`;
|
||||
|
|
48
frontend/tests/unit/common/caniuse_test.js
Normal file
48
frontend/tests/unit/common/caniuse_test.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
import "../fixtures";
|
||||
import {
|
||||
canUseAv1,
|
||||
canUseAvc,
|
||||
canUseHevc,
|
||||
canUseOGV,
|
||||
canUseVideo,
|
||||
canUseVP8,
|
||||
canUseVP9,
|
||||
canUseWebm,
|
||||
} from "common/caniuse";
|
||||
|
||||
let chai = require("chai/chai");
|
||||
let assert = chai.assert;
|
||||
|
||||
describe("common/caniuse", () => {
|
||||
it("canUseVideo", () => {
|
||||
assert.equal(canUseVideo, true);
|
||||
});
|
||||
|
||||
it("canUseAvc", () => {
|
||||
assert.equal(canUseAvc, true);
|
||||
});
|
||||
|
||||
it("canUseOGV", () => {
|
||||
assert.equal(canUseOGV, true);
|
||||
});
|
||||
|
||||
it("canUseVP8", () => {
|
||||
assert.equal(canUseVP8, true);
|
||||
});
|
||||
|
||||
it("canUseVP9", () => {
|
||||
assert.equal(canUseVP9, true);
|
||||
});
|
||||
|
||||
it("canUseAv1", () => {
|
||||
assert.equal(canUseAv1, true);
|
||||
});
|
||||
|
||||
it("canUseWebm", () => {
|
||||
assert.equal(canUseWebm, true);
|
||||
});
|
||||
|
||||
it("canUseHevc", () => {
|
||||
assert.equal(canUseHevc, false);
|
||||
});
|
||||
});
|
|
@ -11,7 +11,6 @@ import (
|
|||
|
||||
const (
|
||||
ContentTypeAvc = `video/mp4; codecs="avc1"`
|
||||
ContentTypeHvc = `video/mp4; codecs="hvc1"`
|
||||
)
|
||||
|
||||
// AddCacheHeader adds a cache control header to the response.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/video"
|
||||
|
@ -65,28 +66,32 @@ func GetVideo(router *gin.RouterGroup) {
|
|||
fileName := photoprism.FileName(f.FileRoot, f.FileName)
|
||||
|
||||
if mf, err := photoprism.NewMediaFile(fileName); err != nil {
|
||||
log.Errorf("video: file %s is missing", clean.Log(f.FileName))
|
||||
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
|
||||
|
||||
// Set missing flag so that the file doesn't show up in search results anymore.
|
||||
logError("video", f.Update("FileMissing", true))
|
||||
|
||||
return
|
||||
} else if f.FileCodec != string(format.Codec) {
|
||||
// Log error and default to 404.mp4
|
||||
log.Errorf("video: file %s is missing", clean.Log(f.FileName))
|
||||
fileName = service.Config().StaticFile("video/404.mp4")
|
||||
AddContentTypeHeader(c, ContentTypeAvc)
|
||||
} else if f.FileCodec != "" && f.FileCodec == string(format.Codec) || format.Codec == video.UnknownCodec && f.FileType == string(format.File) {
|
||||
if f.FileCodec != "" && f.FileCodec != f.FileType {
|
||||
log.Debugf("video: %s has matching codec %s", clean.Log(f.FileName), clean.Log(f.FileCodec))
|
||||
AddContentTypeHeader(c, fmt.Sprintf("%s; codecs=\"%s\"", f.FileMime, clean.Codec(f.FileCodec)))
|
||||
} else {
|
||||
log.Debugf("video: %s has matching type %s", clean.Log(f.FileName), clean.Log(f.FileType))
|
||||
AddContentTypeHeader(c, f.FileMime)
|
||||
}
|
||||
} else {
|
||||
conv := service.Convert()
|
||||
|
||||
if avcFile, err := conv.ToAvc(mf, service.Config().FFmpegEncoder(), false, false); err != nil {
|
||||
// Log error and default to 404.mp4
|
||||
log.Errorf("video: transcoding %s failed", clean.Log(f.FileName))
|
||||
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
|
||||
return
|
||||
fileName = service.Config().StaticFile("video/404.mp4")
|
||||
} else {
|
||||
fileName = avcFile.FileName()
|
||||
}
|
||||
}
|
||||
|
||||
if video.Types[formatName] == video.HEVC {
|
||||
AddContentTypeHeader(c, ContentTypeHvc)
|
||||
} else {
|
||||
AddContentTypeHeader(c, ContentTypeAvc)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetVideo(t *testing.T) {
|
||||
t.Run("ContentTypeAvc", func(t *testing.T) {
|
||||
assert.Equal(t, ContentTypeAvc, fmt.Sprintf("%s; codecs=\"%s\"", "video/mp4", clean.Codec("avc1")))
|
||||
})
|
||||
|
||||
t.Run("invalid hash", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetVideo(router)
|
||||
|
|
|
@ -69,11 +69,16 @@ func (c *Config) TemplateName() string {
|
|||
return "index.tmpl"
|
||||
}
|
||||
|
||||
// StaticPath returns the static assets path.
|
||||
// StaticPath returns the static assets' path.
|
||||
func (c *Config) StaticPath() string {
|
||||
return filepath.Join(c.AssetsPath(), "static")
|
||||
}
|
||||
|
||||
// StaticFile returns the path to a static file.
|
||||
func (c *Config) StaticFile(fileName string) string {
|
||||
return filepath.Join(c.AssetsPath(), "static", fileName)
|
||||
}
|
||||
|
||||
// BuildPath returns the static build path.
|
||||
func (c *Config) BuildPath() string {
|
||||
return filepath.Join(c.StaticPath(), "build")
|
||||
|
|
|
@ -231,6 +231,13 @@ func TestConfig_StaticPath(t *testing.T) {
|
|||
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/static", path)
|
||||
}
|
||||
|
||||
func TestConfig_StaticFile(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
path := c.StaticFile("video/404.mp4")
|
||||
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/static/video/404.mp4", path)
|
||||
}
|
||||
|
||||
func TestConfig_BuildPath(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ import (
|
|||
)
|
||||
|
||||
const CodecUnknown = ""
|
||||
const CodecAv1 = string(video.CodecAV1)
|
||||
const CodecVP9 = string(video.CodecVP9)
|
||||
const CodecAvc1 = string(video.CodecAVC)
|
||||
const CodecJpeg = "jpeg"
|
||||
const CodecHeic = "heic"
|
||||
|
|
|
@ -25,7 +25,7 @@ type Data struct {
|
|||
Duration time.Duration `meta:"Duration,MediaDuration,TrackDuration"`
|
||||
FPS float64 `meta:"VideoFrameRate,VideoAvgFrameRate"`
|
||||
Frames int `meta:"FrameCount"`
|
||||
Codec string `meta:"CompressorID,FileType"`
|
||||
Codec string `meta:"CompressorID,VideoCodecID,CodecID,FileType"`
|
||||
Title string `meta:"Headline,Title" xmp:"dc:title" dc:"title,title.Alt"`
|
||||
Subject string `meta:"Subject,PersonInImage,ObjectName,HierarchicalSubject,CatalogSets" xmp:"Subject"`
|
||||
Keywords Keywords `meta:"Keywords"`
|
||||
|
|
|
@ -9,6 +9,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/video"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/projection"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
@ -292,10 +294,14 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Normalize compression information.
|
||||
// Normalize codec name.
|
||||
data.Codec = strings.ToLower(data.Codec)
|
||||
if strings.Contains(data.Codec, CodecJpeg) {
|
||||
if strings.Contains(data.Codec, CodecJpeg) { // JPEG Image?
|
||||
data.Codec = CodecJpeg
|
||||
} else if c, ok := video.Codecs[data.Codec]; ok { // Video codec?
|
||||
data.Codec = string(c)
|
||||
} else if strings.HasPrefix(data.Codec, "a_") { // Audio codec?
|
||||
data.Codec = ""
|
||||
}
|
||||
|
||||
// Validate and normalize optional DocumentID.
|
||||
|
|
|
@ -4,11 +4,10 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/video"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/projection"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/photoprism/photoprism/pkg/video"
|
||||
)
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
|
@ -40,6 +39,86 @@ func TestJSON(t *testing.T) {
|
|||
assert.Equal(t, "", data.LensModel)
|
||||
})
|
||||
|
||||
t.Run("yoga-av1.webm.json", func(t *testing.T) {
|
||||
data, err := JSON("testdata/yoga-av1.webm.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "yoga-av1.webm", data.FileName)
|
||||
assert.Equal(t, "", data.Codec)
|
||||
assert.Equal(t, "20s", data.Duration.String())
|
||||
assert.Equal(t, 854, data.Width)
|
||||
assert.Equal(t, 480, data.Height)
|
||||
assert.Equal(t, 854, data.ActualWidth())
|
||||
assert.Equal(t, 480, data.ActualHeight())
|
||||
})
|
||||
|
||||
t.Run("stream.webm.json", func(t *testing.T) {
|
||||
data, err := JSON("testdata/stream.webm.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "stream.webm", data.FileName)
|
||||
assert.Equal(t, CodecAv1, data.Codec)
|
||||
assert.Equal(t, "2m24s", data.Duration.String())
|
||||
assert.Equal(t, 1280, data.Width)
|
||||
assert.Equal(t, 720, data.Height)
|
||||
assert.Equal(t, 1280, data.ActualWidth())
|
||||
assert.Equal(t, 720, data.ActualHeight())
|
||||
})
|
||||
|
||||
t.Run("earth.ogv.json", func(t *testing.T) {
|
||||
data, err := JSON("testdata/earth.ogv.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "earth.ogv", data.FileName)
|
||||
assert.Equal(t, string(video.CodecOGV), data.Codec)
|
||||
assert.Equal(t, "0s", data.Duration.String())
|
||||
assert.Equal(t, 1280, data.Width)
|
||||
assert.Equal(t, 720, data.Height)
|
||||
assert.Equal(t, 1280, data.ActualWidth())
|
||||
assert.Equal(t, 720, data.ActualHeight())
|
||||
})
|
||||
|
||||
t.Run("webm-vp8.json", func(t *testing.T) {
|
||||
data, err := JSON("testdata/webm-vp8.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "earth.vp8.webm", data.FileName)
|
||||
assert.Equal(t, string(video.CodecVP8), data.Codec)
|
||||
assert.Equal(t, "30s", data.Duration.String())
|
||||
assert.Equal(t, 1920, data.Width)
|
||||
assert.Equal(t, 1080, data.Height)
|
||||
assert.Equal(t, 1920, data.ActualWidth())
|
||||
assert.Equal(t, 1080, data.ActualHeight())
|
||||
})
|
||||
|
||||
t.Run("webm-vp9.json", func(t *testing.T) {
|
||||
data, err := JSON("testdata/webm-vp9.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "earth-animation.ogv.720p.vp9.webm", data.FileName)
|
||||
assert.Equal(t, string(video.CodecVP9), data.Codec)
|
||||
assert.Equal(t, "8s", data.Duration.String())
|
||||
assert.Equal(t, 1280, data.Width)
|
||||
assert.Equal(t, 720, data.Height)
|
||||
assert.Equal(t, 1280, data.ActualWidth())
|
||||
assert.Equal(t, 720, data.ActualHeight())
|
||||
})
|
||||
|
||||
t.Run("gopher-telegram.json", func(t *testing.T) {
|
||||
data, err := JSON("testdata/gopher-telegram.json", "")
|
||||
|
||||
|
|
28
internal/meta/testdata/earth.ogv.json
vendored
Normal file
28
internal/meta/testdata/earth.ogv.json
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
[{
|
||||
"SourceFile": "earth.ogv",
|
||||
"ExifToolVersion": 12.42,
|
||||
"FileName": "earth.ogv",
|
||||
"Directory": ".",
|
||||
"FileSize": 6397571,
|
||||
"FileModifyDate": "2022:06:24 03:16:55+00:00",
|
||||
"FileAccessDate": "2022:06:24 03:18:39+00:00",
|
||||
"FileInodeChangeDate": "2022:06:24 03:18:39+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "OGV",
|
||||
"FileTypeExtension": "OGV",
|
||||
"MIMEType": "video/ogg",
|
||||
"TheoraVersion": "3 2 0",
|
||||
"ImageWidth": 1280,
|
||||
"ImageHeight": 720,
|
||||
"XOffset": 0,
|
||||
"YOffset": 0,
|
||||
"FrameRate": 30,
|
||||
"ColorSpace": 1,
|
||||
"NominalVideoBitrate": 0,
|
||||
"Quality": 63,
|
||||
"PixelFormat": 0,
|
||||
"Vendor": "Xiph.Org libTheora I 20060526 3 2 0",
|
||||
"Encoder": "ffmpeg2theora 0.19",
|
||||
"ImageSize": "1280 720",
|
||||
"Megapixels": 0.9216
|
||||
}]
|
30
internal/meta/testdata/stream.webm.json
vendored
Normal file
30
internal/meta/testdata/stream.webm.json
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
[{
|
||||
"SourceFile": "stream.webm",
|
||||
"ExifToolVersion": 12.42,
|
||||
"FileName": "stream.webm",
|
||||
"Directory": ".",
|
||||
"FileSize": 57052418,
|
||||
"FileModifyDate": "2022:06:24 01:43:22+00:00",
|
||||
"FileAccessDate": "2022:06:24 01:43:39+00:00",
|
||||
"FileInodeChangeDate": "2022:06:24 01:44:40+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "WEBM",
|
||||
"FileTypeExtension": "WEBM",
|
||||
"MIMEType": "video/webm",
|
||||
"EBMLVersion": 1,
|
||||
"EBMLReadVersion": 1,
|
||||
"DocType": "webm",
|
||||
"DocTypeVersion": 4,
|
||||
"DocTypeReadVersion": 2,
|
||||
"TimecodeScale": 0.001,
|
||||
"Duration": 144.12,
|
||||
"MuxingApp": "libwebm-0.2.1.0",
|
||||
"WritingApp": "aomenc 1.0.0",
|
||||
"TrackNumber": 1,
|
||||
"TrackType": 1,
|
||||
"VideoCodecID": "V_AV1",
|
||||
"ImageWidth": 1280,
|
||||
"ImageHeight": 720,
|
||||
"ImageSize": "1280 720",
|
||||
"Megapixels": 0.9216
|
||||
}]
|
32
internal/meta/testdata/webm-vp8.json
vendored
Normal file
32
internal/meta/testdata/webm-vp8.json
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
[{
|
||||
"SourceFile": "earth.vp8.webm",
|
||||
"ExifToolVersion": 12.42,
|
||||
"FileName": "earth.vp8.webm",
|
||||
"Directory": ".",
|
||||
"FileSize": 2962571,
|
||||
"FileModifyDate": "2022:06:24 03:17:47+00:00",
|
||||
"FileAccessDate": "2022:06:24 03:18:39+00:00",
|
||||
"FileInodeChangeDate": "2022:06:24 03:18:39+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "WEBM",
|
||||
"FileTypeExtension": "WEBM",
|
||||
"MIMEType": "video/webm",
|
||||
"EBMLVersion": 1,
|
||||
"EBMLReadVersion": 1,
|
||||
"DocType": "webm",
|
||||
"DocTypeVersion": 2,
|
||||
"DocTypeReadVersion": 2,
|
||||
"TimecodeScale": 0.001,
|
||||
"MuxingApp": "Lavf56.40.101",
|
||||
"WritingApp": "Lavf56.40.101",
|
||||
"Duration": 30,
|
||||
"TrackNumber": 1,
|
||||
"TrackLanguage": "eng",
|
||||
"CodecID": "V_VP8",
|
||||
"TrackType": 1,
|
||||
"VideoFrameRate": 30.0000003,
|
||||
"ImageWidth": 1920,
|
||||
"ImageHeight": 1080,
|
||||
"ImageSize": "1920 1080",
|
||||
"Megapixels": 2.0736
|
||||
}]
|
33
internal/meta/testdata/webm-vp9.json
vendored
Normal file
33
internal/meta/testdata/webm-vp9.json
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
[{
|
||||
"SourceFile": "earth-animation.ogv.720p.vp9.webm",
|
||||
"ExifToolVersion": 12.42,
|
||||
"FileName": "earth-animation.ogv.720p.vp9.webm",
|
||||
"Directory": ".",
|
||||
"FileSize": 1806960,
|
||||
"FileModifyDate": "2022:06:24 02:37:55+00:00",
|
||||
"FileAccessDate": "2022:06:24 02:40:20+00:00",
|
||||
"FileInodeChangeDate": "2022:06:24 02:40:20+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "WEBM",
|
||||
"FileTypeExtension": "WEBM",
|
||||
"MIMEType": "video/webm",
|
||||
"EBMLVersion": 1,
|
||||
"EBMLReadVersion": 1,
|
||||
"DocType": "webm",
|
||||
"DocTypeVersion": 2,
|
||||
"DocTypeReadVersion": 2,
|
||||
"TimecodeScale": 0.001,
|
||||
"MuxingApp": "Lavf57.56.101",
|
||||
"WritingApp": "Lavf57.56.101",
|
||||
"Duration": 8.033,
|
||||
"TrackNumber": 1,
|
||||
"TrackLanguage": "und",
|
||||
"CodecID": "V_VP9",
|
||||
"TrackType": 1,
|
||||
"VideoFrameRate": 30.0000003,
|
||||
"ImageWidth": 1280,
|
||||
"ImageHeight": 720,
|
||||
"VideoScanType": 2,
|
||||
"ImageSize": "1280 720",
|
||||
"Megapixels": 0.9216
|
||||
}]
|
37
internal/meta/testdata/yoga-av1.webm.json
vendored
Normal file
37
internal/meta/testdata/yoga-av1.webm.json
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
[{
|
||||
"SourceFile": "yoga-av1.webm",
|
||||
"ExifToolVersion": 12.42,
|
||||
"FileName": "yoga-av1.webm",
|
||||
"Directory": ".",
|
||||
"FileSize": 1826192,
|
||||
"FileModifyDate": "2022:06:24 01:25:23+00:00",
|
||||
"FileAccessDate": "2022:06:24 01:26:04+00:00",
|
||||
"FileInodeChangeDate": "2022:06:24 01:26:04+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "WEBM",
|
||||
"FileTypeExtension": "WEBM",
|
||||
"MIMEType": "video/webm",
|
||||
"EBMLVersion": 1,
|
||||
"EBMLReadVersion": 1,
|
||||
"DocType": "webm",
|
||||
"DocTypeVersion": 4,
|
||||
"DocTypeReadVersion": 2,
|
||||
"TimecodeScale": 0.001,
|
||||
"MuxingApp": "Lavf58.37.100",
|
||||
"WritingApp": "Lavf58.37.100",
|
||||
"Duration": 20.302,
|
||||
"VideoFrameRate": 25,
|
||||
"ImageWidth": 854,
|
||||
"ImageHeight": 480,
|
||||
"TrackNumber": 2,
|
||||
"TrackLanguage": "eng",
|
||||
"CodecID": "A_OPUS",
|
||||
"TrackType": 2,
|
||||
"AudioChannels": 2,
|
||||
"AudioSampleRate": 48000,
|
||||
"AudioBitsPerSample": 32,
|
||||
"TagName": "DURATION",
|
||||
"TagString": "00:00:20.302000000",
|
||||
"ImageSize": "854 480",
|
||||
"Megapixels": 0.40992
|
||||
}]
|
23
pkg/clean/codec.go
Normal file
23
pkg/clean/codec.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package clean
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Codec removes non-alphanumeric characters from a string and returns it.
|
||||
func Codec(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Remove unwanted characters.
|
||||
s = strings.Map(func(r rune) rune {
|
||||
if (r < '0' || r > '9') && (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') && r != '_' {
|
||||
return -1
|
||||
}
|
||||
|
||||
return r
|
||||
}, s)
|
||||
|
||||
return s
|
||||
}
|
28
pkg/clean/codec_test.go
Normal file
28
pkg/clean/codec_test.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package clean
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCodec(t *testing.T) {
|
||||
t.Run("UUID", func(t *testing.T) {
|
||||
assert.Equal(t, "123e4567e89b12d3A456426614174000", Codec("123e4567-e89b-12d3-A456-426614174000 "))
|
||||
})
|
||||
t.Run("left_224", func(t *testing.T) {
|
||||
assert.Equal(t, "left_224", Codec("left_224"))
|
||||
})
|
||||
t.Run("VP09", func(t *testing.T) {
|
||||
assert.Equal(t, "VP09", Codec("VP09"))
|
||||
})
|
||||
t.Run("v_vp9", func(t *testing.T) {
|
||||
assert.Equal(t, "v_vp9", Codec("v_vp9"))
|
||||
})
|
||||
t.Run("SHA1", func(t *testing.T) {
|
||||
assert.Equal(t, "5c50ae14f339364eb8224f23c2d3abc7e79016f3READMEmd", Codec("5c50ae14f339364eb8224f23c2d3abc7e79016f3 README.md"))
|
||||
})
|
||||
t.Run("Quotes", func(t *testing.T) {
|
||||
assert.Equal(t, "fooBaaar23", Codec("\"foo\" Baa'ar 2```3"))
|
||||
})
|
||||
}
|
|
@ -16,6 +16,9 @@ func TestIdString(t *testing.T) {
|
|||
t.Run("SHA1", func(t *testing.T) {
|
||||
assert.Equal(t, "5c50ae14f339364eb8224f23c2d3abc7e79016f3readmemd", IdString("5c50ae14f339364eb8224f23c2d3abc7e79016f3 README.md"))
|
||||
})
|
||||
t.Run("Quotes", func(t *testing.T) {
|
||||
assert.Equal(t, "foobaaar23", IdString("\"foo\" baa'ar 2```3"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestIdUint(t *testing.T) {
|
||||
|
|
|
@ -2,25 +2,48 @@ package video
|
|||
|
||||
type Codec string
|
||||
|
||||
// Check browser support: https://cconcolato.github.io/media-mime-support/
|
||||
|
||||
const (
|
||||
UnknownCodec Codec = ""
|
||||
CodecAVC Codec = "avc1"
|
||||
CodecHEVC Codec = "hvc1"
|
||||
CodecVVC Codec = "vvc"
|
||||
CodecAV1 Codec = "av01"
|
||||
CodecVP8 Codec = "vp8"
|
||||
CodecVP9 Codec = "vp9"
|
||||
CodecOGV Codec = "ogv"
|
||||
CodecWebM Codec = "webm"
|
||||
)
|
||||
|
||||
// Codecs maps identifiers to codecs.
|
||||
var Codecs = StandardCodecs{
|
||||
"": UnknownCodec,
|
||||
"a_opus": UnknownCodec,
|
||||
"a_vorbis": UnknownCodec,
|
||||
"avc": CodecAVC,
|
||||
"avc1": CodecAVC,
|
||||
"hvc1": CodecHEVC,
|
||||
"hvc": CodecHEVC,
|
||||
"v_avc": CodecAVC,
|
||||
"v_avc1": CodecAVC,
|
||||
"hevc": CodecHEVC,
|
||||
"hvc": CodecHEVC,
|
||||
"hvc1": CodecHEVC,
|
||||
"v_hvc": CodecHEVC,
|
||||
"v_hvc1": CodecHEVC,
|
||||
"vvc": CodecVVC,
|
||||
"v_vvc": CodecVVC,
|
||||
"av1": CodecAV1,
|
||||
"av01": CodecAV1,
|
||||
"v_av1": CodecAV1,
|
||||
"v_av01": CodecAV1,
|
||||
"vp8": CodecVP8,
|
||||
"vp80": CodecVP8,
|
||||
"v_vp8": CodecVP8,
|
||||
"vp9": CodecVP9,
|
||||
"vp90": CodecVP9,
|
||||
"v_vp9": CodecVP9,
|
||||
"ogv": CodecOGV,
|
||||
"webm": CodecWebM,
|
||||
}
|
||||
|
||||
// StandardCodecs maps names to known codecs.
|
||||
|
|
|
@ -12,8 +12,15 @@ var Types = Standards{
|
|||
"hevc": HEVC,
|
||||
"vvc": VVC,
|
||||
"vvc1": VVC,
|
||||
"vp8": VP8,
|
||||
"vp80": VP8,
|
||||
"vp9": VP9,
|
||||
"vp90": VP9,
|
||||
"av1": AV1,
|
||||
"av01": AV1,
|
||||
"ogg": OGV,
|
||||
"ogv": OGV,
|
||||
"webm": WebM,
|
||||
}
|
||||
|
||||
// Standards maps names to standardized formats.
|
||||
|
|
|
@ -22,15 +22,6 @@ var AVC = Type{
|
|||
Public: true,
|
||||
}
|
||||
|
||||
// AV1 aka AOMedia Video 1.
|
||||
var AV1 = Type{
|
||||
File: fs.VideoAV1,
|
||||
Codec: CodecAV1,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// HEVC aka High Efficiency Video Coding (H.265).
|
||||
var HEVC = Type{
|
||||
File: fs.VideoHEVC,
|
||||
|
@ -48,3 +39,48 @@ var VVC = Type{
|
|||
Height: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// VP8 + Google WebM.
|
||||
var VP8 = Type{
|
||||
File: fs.VideoWebM,
|
||||
Codec: CodecVP8,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// VP9 + Google WebM.
|
||||
var VP9 = Type{
|
||||
File: fs.VideoWebM,
|
||||
Codec: CodecVP9,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// AV1 + Google WebM.
|
||||
var AV1 = Type{
|
||||
File: fs.VideoWebM,
|
||||
Codec: CodecAV1,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// OGV aka Ogg/Theora.
|
||||
var OGV = Type{
|
||||
File: fs.VideoOGV,
|
||||
Codec: CodecOGV,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// WebM Container.
|
||||
var WebM = Type{
|
||||
File: fs.VideoWebM,
|
||||
Codec: UnknownCodec,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue