mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
8c3ec7435e
commit
420fa9946c
37 changed files with 770 additions and 393 deletions
|
|
@ -23,25 +23,33 @@ Additional information can be found in our Developer Guide:
|
|||
|
||||
*/
|
||||
|
||||
export const ContentTypeAVC = 'video/mp4; codecs="avc1"';
|
||||
export const ContentTypeHEVC = 'video/mp4; codecs="hvc1.1.6.L93.90"';
|
||||
export const ContentTypeOGG = "video/ogg";
|
||||
export const ContentTypeWebM = "video/webm";
|
||||
export const ContentTypeVP8 = 'video/webm; codecs="vp8"';
|
||||
export const ContentTypeVP9 = 'video/webm; codecs="vp9"';
|
||||
export const ContentTypeAV1 = 'video/webm; codecs="av01.0.08M.08"';
|
||||
|
||||
export const canUseVideo = !!document.createElement("video").canPlayType;
|
||||
export const canUseAvc = canUseVideo // AVC
|
||||
? !!document.createElement("video").canPlayType('video/mp4; codecs="avc1"')
|
||||
? !!document.createElement("video").canPlayType(ContentTypeAVC)
|
||||
: false;
|
||||
export const canUseHevc = canUseVideo // HEVC, Basic Support
|
||||
? !!document.createElement("video").canPlayType(ContentTypeHEVC)
|
||||
: false;
|
||||
export const canUseOGV = canUseVideo // Ogg Theora
|
||||
? !!document.createElement("video").canPlayType("video/ogg")
|
||||
? !!document.createElement("video").canPlayType(ContentTypeOGG)
|
||||
: false;
|
||||
export const canUseVP8 = canUseVideo // Google WebM, VP8
|
||||
? !!document.createElement("video").canPlayType('video/webm; codecs="vp8"')
|
||||
? !!document.createElement("video").canPlayType(ContentTypeVP8)
|
||||
: false;
|
||||
export const canUseVP9 = canUseVideo // Google WebM, VP9
|
||||
? !!document.createElement("video").canPlayType('video/webm; codecs="vp9"')
|
||||
? !!document.createElement("video").canPlayType(ContentTypeVP9)
|
||||
: false;
|
||||
export const canUseAv1 = canUseVideo // AV1, Main Profile
|
||||
? !!document.createElement("video").canPlayType('video/webm; codecs="av01.0.08M.08"')
|
||||
? !!document.createElement("video").canPlayType(ContentTypeAV1)
|
||||
: false;
|
||||
export const canUseWebM = canUseVideo // Google WebM
|
||||
? !!document.createElement("video").canPlayType("video/webm")
|
||||
: false;
|
||||
export const canUseHevc = canUseVideo // HVC, Basic Support
|
||||
? !!document.createElement("video").canPlayType('video/mp4; codecs="hvc1.1.6.L93.90"')
|
||||
? !!document.createElement("video").canPlayType(ContentTypeWebM)
|
||||
: false;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,21 @@ Additional information can be found in our Developer Guide:
|
|||
|
||||
*/
|
||||
|
||||
import { canUseAv1, canUseHevc, canUseOGV, canUseVP8, canUseVP9, canUseWebM } from "./caniuse";
|
||||
import {
|
||||
canUseAv1,
|
||||
canUseHevc,
|
||||
canUseOGV,
|
||||
canUseVP8,
|
||||
canUseVP9,
|
||||
canUseWebM,
|
||||
ContentTypeAVC,
|
||||
ContentTypeHEVC,
|
||||
ContentTypeOGG,
|
||||
ContentTypeWebM,
|
||||
ContentTypeVP8,
|
||||
ContentTypeVP9,
|
||||
ContentTypeAV1,
|
||||
} from "./caniuse";
|
||||
import { config } from "app/session";
|
||||
import {
|
||||
DATE_FULL,
|
||||
|
|
@ -34,10 +48,11 @@ import {
|
|||
CodecOGV,
|
||||
CodecVP8,
|
||||
CodecVP9,
|
||||
FormatAv1,
|
||||
FormatAvc,
|
||||
FormatHevc,
|
||||
FormatAV1,
|
||||
FormatAVC,
|
||||
FormatHEVC,
|
||||
FormatWebM,
|
||||
CodecOGG,
|
||||
} from "model/photo";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
import { DateTime } from "luxon";
|
||||
|
|
@ -545,46 +560,59 @@ export default class Util {
|
|||
|
||||
return "fit_7680";
|
||||
}
|
||||
static videoFormat(codec) {
|
||||
if (!codec) {
|
||||
return FormatAvc;
|
||||
} else if (canUseHevc && (codec === CodecHvc1 || codec === CodecHev1)) {
|
||||
return FormatHevc;
|
||||
} else if (canUseOGV && codec === CodecOGV) {
|
||||
|
||||
static videoFormat(codec, mime) {
|
||||
if (!codec && !mime) {
|
||||
return FormatAVC;
|
||||
} else if (canUseHevc && (codec === CodecHvc1 || codec === CodecHev1 || mime === ContentTypeHEVC)) {
|
||||
return FormatHEVC;
|
||||
} else if (canUseOGV && (codec === CodecOGV || codec === CodecOGG || mime === ContentTypeOGG)) {
|
||||
return CodecOGV;
|
||||
} else if (canUseVP8 && codec === CodecVP8) {
|
||||
} else if (canUseVP8 && (codec === CodecVP8 || mime === ContentTypeVP8)) {
|
||||
return CodecVP8;
|
||||
} else if (canUseVP9 && codec === CodecVP9) {
|
||||
} else if (canUseVP9 && (codec === CodecVP9 || mime === ContentTypeVP9)) {
|
||||
return CodecVP9;
|
||||
} else if (canUseAv1 && (codec === CodecAv01 || codec === CodecAv1C)) {
|
||||
return FormatAv1;
|
||||
} else if (canUseWebM && codec === FormatWebM) {
|
||||
} else if (canUseAv1 && (codec === CodecAv01 || codec === CodecAv1C || mime === ContentTypeAV1)) {
|
||||
return FormatAV1;
|
||||
} else if (canUseWebM && (codec === FormatWebM || mime === ContentTypeWebM)) {
|
||||
return FormatWebM;
|
||||
}
|
||||
|
||||
return FormatAvc;
|
||||
return FormatAVC;
|
||||
}
|
||||
|
||||
static videoUrl(hash, codec) {
|
||||
return `${config.videoUri}/videos/${hash}/${config.previewToken}/${this.videoFormat(codec)}`;
|
||||
static videoFormatUrl(hash, format) {
|
||||
if (!hash) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!format) {
|
||||
format = FormatAVC;
|
||||
}
|
||||
|
||||
return `${config.videoUri}/videos/${hash}/${config.previewToken}/${format}`;
|
||||
}
|
||||
|
||||
static videoType(codec) {
|
||||
switch (this.videoFormat(codec)) {
|
||||
case FormatAvc:
|
||||
return 'video/mp4; codecs="avc1"';
|
||||
static videoUrl(hash, codec, mime) {
|
||||
return this.videoFormatUrl(hash, this.videoFormat(codec, mime));
|
||||
}
|
||||
|
||||
static videoContentType(codec, mime) {
|
||||
switch (this.videoFormat(codec, mime)) {
|
||||
case FormatAVC:
|
||||
return ContentTypeAVC;
|
||||
case CodecOGV:
|
||||
return "video/ogg";
|
||||
return ContentTypeOGG;
|
||||
case CodecVP8:
|
||||
return 'video/webm; codecs="vp8"';
|
||||
return ContentTypeVP8;
|
||||
case CodecVP9:
|
||||
return 'video/webm; codecs="vp9"';
|
||||
case FormatAv1:
|
||||
return 'video/webm; codecs="av01.0.08M.08"';
|
||||
return ContentTypeVP9;
|
||||
case FormatAV1:
|
||||
return ContentTypeAV1;
|
||||
case FormatWebM:
|
||||
return "video/webm";
|
||||
case FormatHevc:
|
||||
return 'video/mp4; codecs="hvc1.1.6.L93.90"';
|
||||
return ContentTypeWebM;
|
||||
case FormatHEVC:
|
||||
return ContentTypeHEVC;
|
||||
default:
|
||||
return "video/mp4";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@
|
|||
<td>
|
||||
{{ $gettext(`Type`) }}
|
||||
</td>
|
||||
<td class="text-break">{{ file.typeInfo() }}</td>
|
||||
<td class="text-break" :title="file?.Mime">{{ file.typeInfo() }}</td>
|
||||
</tr>
|
||||
<tr v-if="file.isAnimated()">
|
||||
<td>
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@
|
|||
<div class="preview__overlay"></div>
|
||||
<div v-if="m.Type === 'live' || m.Type === 'animated'" class="live-player">
|
||||
<video :id="'live-player-' + m.ID" width="500" height="500" preload="none" loop muted playsinline>
|
||||
<source :type="m.videoType()" :src="m.videoUrl()" />
|
||||
<source :type="m.videoContentType()" :src="m.videoUrl()" />
|
||||
</video>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@
|
|||
<div class="preview__overlay"></div>
|
||||
<div v-if="m.Type === 'live' || m.Type === 'animated'" class="live-player">
|
||||
<video :id="'live-player-' + m.ID" width="224" height="224" preload="none" loop muted playsinline>
|
||||
<source :type="m.videoType()" :src="m.videoUrl()" />
|
||||
<source :type="m.videoContentType()" :src="m.videoUrl()" />
|
||||
</video>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ import PhotoSwipeDynamicCaption from "photoswipe-dynamic-caption-plugin";
|
|||
import Util from "common/util";
|
||||
import Api from "common/api";
|
||||
import Thumb from "model/thumb";
|
||||
import { MediaAnimated, MediaLive, Photo } from "model/photo";
|
||||
import { FormatAVC, MediaAnimated, MediaLive, Photo } from "model/photo";
|
||||
import { ContentTypeAVC } from "common/caniuse";
|
||||
|
||||
/*
|
||||
TODO: All previously available features and controls must be preserved in the new hybrid photo/video viewer:
|
||||
|
|
@ -218,6 +219,116 @@ export default {
|
|||
ctx.viewer.loading = false;
|
||||
});
|
||||
},
|
||||
getItemData(el, i) {
|
||||
/*
|
||||
TODO: Rendering of slides needs to be improved to allow dynamic zooming (loading higher resolution thumbs
|
||||
depending on zoom level and screen resolution).
|
||||
*/
|
||||
|
||||
// Get the current slide model data.
|
||||
const model = this.models[i];
|
||||
|
||||
// Get the screen (window) resolution in real pixels
|
||||
const pixels = this.getWindowPixels();
|
||||
|
||||
// Get the right thumbnail size based on the screen resolution in pixels.
|
||||
const thumbSize = Util.thumbSize(pixels.width, pixels.height);
|
||||
|
||||
// Get thumbnail image URL, width, and height.
|
||||
const img = {
|
||||
src: model.Thumbs[thumbSize].src,
|
||||
width: model.Thumbs[thumbSize].w,
|
||||
height: model.Thumbs[thumbSize].h,
|
||||
alt: model?.Title,
|
||||
};
|
||||
|
||||
// Check if content is playable and return the data needed to render it in "contentLoad".
|
||||
if (model?.Playable && model?.Hash) {
|
||||
/*
|
||||
TODO: The server should (additionally) provide a video/animation still from time index 0 that can be used as
|
||||
poster (the current thumbnail is taken later for longer videos, since the first frame is often black).
|
||||
*/
|
||||
|
||||
// Check if the video duration is known and 5 seconds or less.
|
||||
const isShort = model?.Duration ? model.Duration > 0 && model.Duration <= 5000000000 : false;
|
||||
|
||||
// Set the slide data needed to render and play the video.
|
||||
return {
|
||||
type: "html", // Render custom HTML.
|
||||
html: `<div class="pswp__error-msg">Loading video...</div>`, // Replaced with the <video> element.
|
||||
model: model, // Content model.
|
||||
format: Util.videoFormat(model?.Codec, model?.Mime), // Content format.
|
||||
loop: isShort || model?.Type === MediaAnimated || model?.Type === MediaLive, // If possible, loop these types.
|
||||
msrc: img.src, // Image URL.
|
||||
};
|
||||
}
|
||||
|
||||
// Return the image data so that PhotoSwipe can render it in the viewer,
|
||||
// see https://photoswipe.com/data-sources/#dynamically-generated-data.
|
||||
return img;
|
||||
},
|
||||
onContentLoad(ev) {
|
||||
const { content } = ev;
|
||||
if (content.data?.type === "html") {
|
||||
// Prevent default loading behavior.
|
||||
ev.preventDefault();
|
||||
|
||||
try {
|
||||
// Create video element.
|
||||
content.element = this.createVideoElement(
|
||||
content.data.model,
|
||||
content.data.format,
|
||||
content.data.msrc,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
);
|
||||
content.state = "loading";
|
||||
content.onLoaded();
|
||||
} catch (err) {
|
||||
console.warn("failed to load video", err);
|
||||
}
|
||||
}
|
||||
},
|
||||
// Creates an HTMLMediaElement for playing videos, animations, and live photos.
|
||||
createVideoElement(model, format, posterSrc, autoplay = false, loop = false, mute = false) {
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement.
|
||||
const video = document.createElement("video");
|
||||
|
||||
// Check if a slideshow is running.
|
||||
const slideshow = this.slideshow.active;
|
||||
|
||||
// Set HTMLMediaElement properties.
|
||||
video.className = "pswp__video";
|
||||
video.poster = posterSrc;
|
||||
video.autoplay = autoplay;
|
||||
video.loop = loop && !slideshow;
|
||||
video.mute = mute;
|
||||
video.preload = autoplay ? "auto" : "metadata";
|
||||
video.playsInline = true;
|
||||
video.controls = true;
|
||||
|
||||
// Disable download control is downloads are not allowed.
|
||||
if (!this.canDownload && video.controlsList) {
|
||||
video.controlsList?.add("nodownload");
|
||||
}
|
||||
|
||||
// Create and append video source elements, depending on file format support.
|
||||
if (format !== FormatAVC && model?.Mime && model.Mime !== ContentTypeAVC && video.canPlayType(model.Mime)) {
|
||||
const nativeSource = document.createElement("source");
|
||||
nativeSource.type = model.Mime;
|
||||
nativeSource.src = Util.videoFormatUrl(model.Hash, format);
|
||||
video.appendChild(nativeSource);
|
||||
}
|
||||
|
||||
const avcSource = document.createElement("source");
|
||||
avcSource.type = ContentTypeAVC;
|
||||
avcSource.src = Util.videoFormatUrl(model.Hash, FormatAVC);
|
||||
video.appendChild(avcSource);
|
||||
|
||||
// Return HTMLMediaElement.
|
||||
return video;
|
||||
},
|
||||
// Initializes and opens the PhotoSwipe lightbox with the
|
||||
// images and/or videos that belong to the specified models.
|
||||
renderLightbox(models, index = 0) {
|
||||
|
|
@ -293,81 +404,11 @@ export default {
|
|||
|
||||
// Processes model data for rendering slides with PhotoSwipe,
|
||||
// see https://photoswipe.com/filters/#itemdata.
|
||||
lightbox.addFilter("itemData", (el, i) => {
|
||||
/*
|
||||
TODO: Rendering of slides needs to be improved to allow dynamic zooming (loading higher resolution thumbs
|
||||
depending on zoom level and screen resolution).
|
||||
*/
|
||||
|
||||
// Get the current slide model data.
|
||||
const model = this.models[i];
|
||||
|
||||
// Get the screen (window) resolution in real pixels
|
||||
const pixels = this.getWindowPixels();
|
||||
|
||||
// Get the right thumbnail size based on the screen resolution in pixels.
|
||||
const thumbSize = Util.thumbSize(pixels.width, pixels.height);
|
||||
|
||||
// Get thumbnail image URL, width, and height.
|
||||
const img = {
|
||||
src: model.Thumbs[thumbSize].src,
|
||||
width: model.Thumbs[thumbSize].w,
|
||||
height: model.Thumbs[thumbSize].h,
|
||||
alt: model?.Title,
|
||||
};
|
||||
|
||||
// Check if content is playable and return the data needed to render it in "contentLoad".
|
||||
if (model?.Playable && model?.Hash) {
|
||||
/*
|
||||
TODO: The server should (additionally) provide a video/animation still from time index 0 that can be used as
|
||||
poster (the current thumbnail is taken later for longer videos, since the first frame is often black).
|
||||
*/
|
||||
|
||||
// Check if the video duration is known and 5 seconds or less.
|
||||
const isShort = model?.Duration ? model.Duration > 0 && model.Duration <= 5000000000 : false;
|
||||
|
||||
// Set the slide data needed to render and play the video.
|
||||
return {
|
||||
type: "html", // Render custom HTML.
|
||||
html: `<div class="pswp__error-msg">Loading video...</div>`, // Replaced with the <video> element.
|
||||
model: model, // Thumbnail model.
|
||||
loop: isShort || model?.Type === MediaAnimated || model?.Type === MediaLive, // If possible, loop these types.
|
||||
videoUrl: Util.videoUrl(model.Hash, model?.Codec), // Video URL.
|
||||
videoType: Util.videoType(model?.Codec), // Media Type.
|
||||
msrc: img.src, // Image URL.
|
||||
};
|
||||
}
|
||||
|
||||
// Return the image data so that PhotoSwipe can render it in the viewer,
|
||||
// see https://photoswipe.com/data-sources/#dynamically-generated-data.
|
||||
return img;
|
||||
});
|
||||
lightbox.addFilter("itemData", this.getItemData);
|
||||
|
||||
// Renders content when a slide starts to load (can be default prevented),
|
||||
// see https://photoswipe.com/events/#slide-content-events.
|
||||
lightbox.on("contentLoad", (ev) => {
|
||||
const { content } = ev;
|
||||
if (content.data?.type === "html") {
|
||||
// Prevent default loading behavior.
|
||||
ev.preventDefault();
|
||||
|
||||
try {
|
||||
// Create video element.
|
||||
content.element = this.createVideoElement(
|
||||
content.data.videoUrl,
|
||||
content.data.videoType,
|
||||
content.data.msrc,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
);
|
||||
content.state = "loading";
|
||||
content.onLoaded();
|
||||
} catch (err) {
|
||||
console.warn("failed to load video", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
lightbox.on("contentLoad", this.onContentLoad);
|
||||
|
||||
// Pauses videos, animations, and live photos when slide content becomes active (can be default prevented),
|
||||
// see https://photoswipe.com/events/#slide-content-events.
|
||||
|
|
@ -562,43 +603,6 @@ export default {
|
|||
}
|
||||
});
|
||||
},
|
||||
// Creates an HTMLMediaElement for playing videos, animations, and live photos.
|
||||
createVideoElement(videoSrc, videoType, posterSrc, autoplay = false, loop = false, mute = false) {
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement.
|
||||
const video = document.createElement("video");
|
||||
|
||||
// Check if a slideshow is running.
|
||||
const slideshow = this.slideshow.active;
|
||||
|
||||
// Set HTMLMediaElement properties.
|
||||
video.className = "pswp__video";
|
||||
video.poster = posterSrc;
|
||||
video.autoplay = autoplay;
|
||||
video.loop = loop && !slideshow;
|
||||
video.mute = mute;
|
||||
video.preload = autoplay ? "auto" : "metadata";
|
||||
video.playsInline = true;
|
||||
video.controls = true;
|
||||
|
||||
// Disable download control is downloads are not allowed.
|
||||
if (!this.canDownload && video.controlsList) {
|
||||
video.controlsList?.add("nodownload");
|
||||
}
|
||||
|
||||
// Create and append video source element.
|
||||
/*
|
||||
TODO: Provide alternative sources and/or an m3u8 playlist to let the browser choose the best format (first check
|
||||
best practices on Google and in the code of other open source software, then develop a simple/static proof
|
||||
of concept to see if/how it works).
|
||||
*/
|
||||
const source = document.createElement("source");
|
||||
source.type = videoType;
|
||||
source.src = videoSrc;
|
||||
video.appendChild(source);
|
||||
|
||||
// Return HTMLMediaElement.
|
||||
return video;
|
||||
},
|
||||
// Destroys the PhotoSwipe lightbox instance after use, see onClose().
|
||||
destroyLightbox() {
|
||||
if (this.lightbox) {
|
||||
|
|
|
|||
|
|
@ -37,9 +37,10 @@ import { $gettext } from "common/gettext";
|
|||
import { PhotoClipboard } from "common/clipboard";
|
||||
import download from "common/download";
|
||||
import * as src from "common/src";
|
||||
import { canUseOGV, canUseVP8, canUseVP9, canUseAv1, canUseWebM, canUseHevc } from "common/caniuse";
|
||||
import { ContentTypeAVC } from "common/caniuse";
|
||||
|
||||
export const CodecOGV = "ogv";
|
||||
export const CodecOGG = "ogg";
|
||||
export const CodecVP8 = "vp8";
|
||||
export const CodecVP9 = "vp9";
|
||||
export const CodecAv01 = "av01";
|
||||
|
|
@ -47,15 +48,15 @@ export const CodecAv1C = "av1c";
|
|||
export const CodecAvc1 = "avc1";
|
||||
export const CodecHvc1 = "hvc1";
|
||||
export const CodecHev1 = "hev1";
|
||||
export const FormatMp4 = "mp4";
|
||||
export const FormatAv1 = "av01";
|
||||
export const FormatAvc = "avc";
|
||||
export const FormatHevc = "hevc";
|
||||
export const FormatMP4 = "mp4";
|
||||
export const FormatAV1 = "av01";
|
||||
export const FormatAVC = "avc";
|
||||
export const FormatHEVC = "hevc";
|
||||
export const FormatWebM = "webm";
|
||||
export const FormatJpeg = "jpg";
|
||||
export const FormatPng = "png";
|
||||
export const FormatSvg = "svg";
|
||||
export const FormatGif = "gif";
|
||||
export const FormatJPEG = "jpg";
|
||||
export const FormatPNG = "png";
|
||||
export const FormatSVG = "svg";
|
||||
export const FormatGIF = "gif";
|
||||
export const MediaImage = "image";
|
||||
export const MediaRaw = "raw";
|
||||
export const MediaAnimated = "animated";
|
||||
|
|
@ -200,7 +201,14 @@ export class Photo extends RestModel {
|
|||
}
|
||||
|
||||
classes() {
|
||||
return this.generateClasses(this.isPlayable(), PhotoClipboard.has(this), this.Portrait, this.Favorite, this.Private, this.isStack());
|
||||
return this.generateClasses(
|
||||
this.isPlayable(),
|
||||
PhotoClipboard.has(this),
|
||||
this.Portrait,
|
||||
this.Favorite,
|
||||
this.Private,
|
||||
this.isStack()
|
||||
);
|
||||
}
|
||||
|
||||
generateClasses = memoizeOne((isPlayable, isInClipboard, portrait, favorite, isPrivate, isStack) => {
|
||||
|
|
@ -441,7 +449,7 @@ export class Photo extends RestModel {
|
|||
let jpegs = 0;
|
||||
|
||||
this.Files.forEach((f) => {
|
||||
if (f && f.FileType === FormatJpeg) {
|
||||
if (f && f.FileType === FormatJPEG) {
|
||||
jpegs++;
|
||||
}
|
||||
});
|
||||
|
|
@ -515,7 +523,7 @@ export class Photo extends RestModel {
|
|||
let file = files.find((f) => f.Codec === CodecAvc1);
|
||||
|
||||
if (!file) {
|
||||
file = files.find((f) => f.FileType === FormatMp4);
|
||||
file = files.find((f) => f.FileType === FormatMP4);
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
|
|
@ -534,39 +542,23 @@ export class Photo extends RestModel {
|
|||
return false;
|
||||
}
|
||||
|
||||
return this.Files.find((f) => f.FileType === FormatGif || !!f.Frames || !!f.Duration);
|
||||
return this.Files.find((f) => f.FileType === FormatGIF || !!f.Frames || !!f.Duration);
|
||||
}
|
||||
|
||||
videoContentType() {
|
||||
const file = this.videoFile();
|
||||
|
||||
if (file) {
|
||||
return Util.videoContentType(file?.Codec, file?.Mime);
|
||||
} else {
|
||||
return ContentTypeAVC;
|
||||
}
|
||||
}
|
||||
|
||||
videoUrl() {
|
||||
const file = this.videoFile();
|
||||
|
||||
if (file) {
|
||||
let videoFormat = FormatAvc;
|
||||
const fileCodec = file.Codec ? file.Codec : "";
|
||||
|
||||
if (canUseHevc && (fileCodec === CodecHvc1 || fileCodec === CodecHev1)) {
|
||||
videoFormat = FormatHevc;
|
||||
} else if (canUseOGV && fileCodec === CodecOGV) {
|
||||
videoFormat = CodecOGV;
|
||||
} else if (canUseVP8 && fileCodec === CodecVP8) {
|
||||
videoFormat = CodecVP8;
|
||||
} else if (canUseVP9 && fileCodec === CodecVP9) {
|
||||
videoFormat = CodecVP9;
|
||||
} else if (canUseAv1 && (fileCodec === CodecAv01 || fileCodec === CodecAv1C)) {
|
||||
videoFormat = FormatAv1;
|
||||
} else if (canUseWebM && file.FileType === FormatWebM) {
|
||||
videoFormat = FormatWebM;
|
||||
}
|
||||
|
||||
return `${config.videoUri}/videos/${file.Hash}/${config.previewToken}/${videoFormat}`;
|
||||
}
|
||||
|
||||
return `${config.videoUri}/videos/${this.Hash}/${config.previewToken}/${FormatAvc}`;
|
||||
}
|
||||
|
||||
videoType() {
|
||||
const file = this.videoFile();
|
||||
return Util.videoType(file?.Codec);
|
||||
return Util.videoUrl(file ? file.Hash : this.Hash, file?.Codec, file?.Mime);
|
||||
}
|
||||
|
||||
mainFile() {
|
||||
|
|
@ -587,7 +579,7 @@ export class Photo extends RestModel {
|
|||
}
|
||||
|
||||
// Find and return the first JPEG or PNG image otherwise.
|
||||
file = files.find((f) => f.FileType === FormatJpeg || f.FileType === FormatPng);
|
||||
file = files.find((f) => f.FileType === FormatJPEG || f.FileType === FormatPNG);
|
||||
|
||||
// Found?
|
||||
if (file) {
|
||||
|
|
@ -636,7 +628,7 @@ export class Photo extends RestModel {
|
|||
}
|
||||
|
||||
// Find first original media file with a format other than JPEG.
|
||||
file = files.find((f) => !f.Sidecar && f.FileType !== FormatJpeg && f.Root === "/");
|
||||
file = files.find((f) => !f.Sidecar && f.FileType !== FormatJPEG && f.Root === "/");
|
||||
|
||||
// Found?
|
||||
if (file) {
|
||||
|
|
@ -652,7 +644,7 @@ export class Photo extends RestModel {
|
|||
return [this];
|
||||
}
|
||||
|
||||
return this.Files.filter((f) => f.FileType === FormatJpeg || f.FileType === FormatPng);
|
||||
return this.Files.filter((f) => f.FileType === FormatJPEG || f.FileType === FormatPNG);
|
||||
}
|
||||
|
||||
mainFileHash() {
|
||||
|
|
@ -696,7 +688,14 @@ export class Photo extends RestModel {
|
|||
}
|
||||
|
||||
thumbnailUrl(size) {
|
||||
return this.generateThumbnailUrl(this.mainFileHash(), this.videoFile(), config.staticUri, config.contentUri, config.previewToken, size);
|
||||
return this.generateThumbnailUrl(
|
||||
this.mainFileHash(),
|
||||
this.videoFile(),
|
||||
config.staticUri,
|
||||
config.contentUri,
|
||||
config.previewToken,
|
||||
size
|
||||
);
|
||||
}
|
||||
|
||||
generateThumbnailUrl = memoizeOne((mainFileHash, videoFile, staticUri, contentUri, previewToken, size) => {
|
||||
|
|
@ -900,7 +899,7 @@ export class Photo extends RestModel {
|
|||
return this;
|
||||
}
|
||||
|
||||
return this.Files.find((f) => f.MediaType === MediaVector || f.FileType === FormatSvg);
|
||||
return this.Files.find((f) => f.MediaType === MediaVector || f.FileType === FormatSVG);
|
||||
}
|
||||
|
||||
getVectorInfo = () => {
|
||||
|
|
@ -1010,48 +1009,60 @@ export class Photo extends RestModel {
|
|||
|
||||
// Example: iPhone 12 Pro Max 5.1mm ƒ/1.6, 26mm, ISO32, 1/4525
|
||||
getLensInfo = () => {
|
||||
return this.generateLensInfo(this.Lens, this.LensID, this.LensMake, this.LensModel, this.CameraModel, this.FNumber, this.Iso, this.Exposure, this.FocalLength);
|
||||
return this.generateLensInfo(
|
||||
this.Lens,
|
||||
this.LensID,
|
||||
this.LensMake,
|
||||
this.LensModel,
|
||||
this.CameraModel,
|
||||
this.FNumber,
|
||||
this.Iso,
|
||||
this.Exposure,
|
||||
this.FocalLength
|
||||
);
|
||||
};
|
||||
|
||||
generateLensInfo = memoizeOne((lens, lensId, lensMake, lensModel, cameraModel, fNumber, iso, exposure, focalLength) => {
|
||||
let info = [];
|
||||
const id = lensId ? lensId : lens && lens.ID ? lens.ID : 1;
|
||||
const make = lensMake ? lensMake : lens && lens.Make ? lens.Make : "";
|
||||
const model = (lensModel ? lensModel : lens && lens.Model ? lens.Model : "").replace("f/", "ƒ/");
|
||||
generateLensInfo = memoizeOne(
|
||||
(lens, lensId, lensMake, lensModel, cameraModel, fNumber, iso, exposure, focalLength) => {
|
||||
let info = [];
|
||||
const id = lensId ? lensId : lens && lens.ID ? lens.ID : 1;
|
||||
const make = lensMake ? lensMake : lens && lens.Make ? lens.Make : "";
|
||||
const model = (lensModel ? lensModel : lens && lens.Model ? lens.Model : "").replace("f/", "ƒ/");
|
||||
|
||||
// Example: EF-S18-55mm f/3.5-5.6 IS STM
|
||||
if (id > 1) {
|
||||
if (!model && !!make) {
|
||||
info.push(make);
|
||||
} else if (model.length > 45) {
|
||||
return model;
|
||||
} else if (model) {
|
||||
info.push(model);
|
||||
// Example: EF-S18-55mm f/3.5-5.6 IS STM
|
||||
if (id > 1) {
|
||||
if (!model && !!make) {
|
||||
info.push(make);
|
||||
} else if (model.length > 45) {
|
||||
return model;
|
||||
} else if (model) {
|
||||
info.push(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (focalLength) {
|
||||
info.push(focalLength + "mm");
|
||||
}
|
||||
if (focalLength) {
|
||||
info.push(focalLength + "mm");
|
||||
}
|
||||
|
||||
if (fNumber && (!model || !model.endsWith(fNumber.toString()))) {
|
||||
info.push("ƒ/" + fNumber);
|
||||
}
|
||||
if (fNumber && (!model || !model.endsWith(fNumber.toString()))) {
|
||||
info.push("ƒ/" + fNumber);
|
||||
}
|
||||
|
||||
if (iso && model.length < 27) {
|
||||
info.push("ISO " + iso);
|
||||
}
|
||||
if (iso && model.length < 27) {
|
||||
info.push("ISO " + iso);
|
||||
}
|
||||
|
||||
if (exposure) {
|
||||
info.push(exposure);
|
||||
}
|
||||
if (exposure) {
|
||||
info.push(exposure);
|
||||
}
|
||||
|
||||
if (!info.length) {
|
||||
return $gettext("Unknown");
|
||||
}
|
||||
if (!info.length) {
|
||||
return $gettext("Unknown");
|
||||
}
|
||||
|
||||
return info.join(", ");
|
||||
});
|
||||
return info.join(", ");
|
||||
}
|
||||
);
|
||||
|
||||
getCamera() {
|
||||
if (this.Camera) {
|
||||
|
|
@ -1091,15 +1102,21 @@ export class Photo extends RestModel {
|
|||
}
|
||||
|
||||
primaryFile(fileUID) {
|
||||
return Api.post(`${this.getEntityResource()}/files/${fileUID}/primary`).then((r) => Promise.resolve(this.setValues(r.data)));
|
||||
return Api.post(`${this.getEntityResource()}/files/${fileUID}/primary`).then((r) =>
|
||||
Promise.resolve(this.setValues(r.data))
|
||||
);
|
||||
}
|
||||
|
||||
unstackFile(fileUID) {
|
||||
return Api.post(`${this.getEntityResource()}/files/${fileUID}/unstack`).then((r) => Promise.resolve(this.setValues(r.data)));
|
||||
return Api.post(`${this.getEntityResource()}/files/${fileUID}/unstack`).then((r) =>
|
||||
Promise.resolve(this.setValues(r.data))
|
||||
);
|
||||
}
|
||||
|
||||
deleteFile(fileUID) {
|
||||
return Api.delete(`${this.getEntityResource()}/files/${fileUID}`).then((r) => Promise.resolve(this.setValues(r.data)));
|
||||
return Api.delete(`${this.getEntityResource()}/files/${fileUID}`).then((r) =>
|
||||
Promise.resolve(this.setValues(r.data))
|
||||
);
|
||||
}
|
||||
|
||||
changeFileOrientation(file) {
|
||||
|
|
@ -1117,7 +1134,9 @@ export class Photo extends RestModel {
|
|||
}
|
||||
|
||||
// Change file orientation.
|
||||
return Api.put(`${this.getEntityResource()}/files/${file.UID}/orientation`, values).then((r) => Promise.resolve(this.setValues(r.data)));
|
||||
return Api.put(`${this.getEntityResource()}/files/${file.UID}/orientation`, values).then((r) =>
|
||||
Promise.resolve(this.setValues(r.data))
|
||||
);
|
||||
}
|
||||
|
||||
like() {
|
||||
|
|
@ -1131,15 +1150,21 @@ export class Photo extends RestModel {
|
|||
}
|
||||
|
||||
addLabel(name) {
|
||||
return Api.post(this.getEntityResource() + "/label", { Name: name, Priority: 10 }).then((r) => Promise.resolve(this.setValues(r.data)));
|
||||
return Api.post(this.getEntityResource() + "/label", { Name: name, Priority: 10 }).then((r) =>
|
||||
Promise.resolve(this.setValues(r.data))
|
||||
);
|
||||
}
|
||||
|
||||
activateLabel(id) {
|
||||
return Api.put(this.getEntityResource() + "/label/" + id, { Uncertainty: 0 }).then((r) => Promise.resolve(this.setValues(r.data)));
|
||||
return Api.put(this.getEntityResource() + "/label/" + id, { Uncertainty: 0 }).then((r) =>
|
||||
Promise.resolve(this.setValues(r.data))
|
||||
);
|
||||
}
|
||||
|
||||
renameLabel(id, name) {
|
||||
return Api.put(this.getEntityResource() + "/label/" + id, { Label: { Name: name } }).then((r) => Promise.resolve(this.setValues(r.data)));
|
||||
return Api.put(this.getEntityResource() + "/label/" + id, { Label: { Name: name } }).then((r) =>
|
||||
Promise.resolve(this.setValues(r.data))
|
||||
);
|
||||
}
|
||||
|
||||
removeLabel(id) {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ export class Thumb extends Model {
|
|||
Width: 0,
|
||||
Height: 0,
|
||||
Hash: "",
|
||||
Codec: "",
|
||||
Mime: "",
|
||||
Thumbs: {},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import "../fixtures";
|
||||
import Util from "common/util";
|
||||
import {canUseHevc, canUseWebM} from "common/caniuse";
|
||||
|
||||
let chai = require("chai/chai");
|
||||
let assert = chai.assert;
|
||||
|
|
@ -45,6 +46,24 @@ describe("common/util", () => {
|
|||
const iPhone13 = Util.formatCamera(null, 21, "Apple", "iPhone 13");
|
||||
assert.equal(iPhone13, "iPhone 13");
|
||||
});
|
||||
it("should return matching video format name", () => {
|
||||
const avc = Util.videoFormat("avc1", "video/mp4");
|
||||
assert.equal(avc, "avc");
|
||||
|
||||
const hvc = Util.videoFormat("hvc1", "video/mp4");
|
||||
if (canUseHevc) {
|
||||
assert.equal(hvc, "hvc");
|
||||
} else {
|
||||
assert.equal(hvc, "avc");
|
||||
}
|
||||
|
||||
const webm = Util.videoFormat("", "video/webm");
|
||||
if (canUseWebM) {
|
||||
assert.equal(webm, "webm");
|
||||
} else {
|
||||
assert.equal(webm, "avc");
|
||||
}
|
||||
});
|
||||
it("should convert -1 to roman", () => {
|
||||
const roman = Util.arabicToRoman(-1);
|
||||
assert.equal(roman, "");
|
||||
|
|
@ -75,6 +94,9 @@ describe("common/util", () => {
|
|||
});
|
||||
it("should encode link", () => {
|
||||
const result = Util.encodeHTML("Try this: https://photoswipe.com/options/?foo=bar&bar=baz. It's a link!");
|
||||
assert.equal(result, `Try this: <a href="https://photoswipe.com/options/" target="_blank">https://photoswipe.com/options/</a> It's a link!`);
|
||||
assert.equal(
|
||||
result,
|
||||
`Try this: <a href="https://photoswipe.com/options/" target="_blank">https://photoswipe.com/options/</a> It's a link!`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import "../fixtures";
|
||||
import { Photo, BatchSize, FormatJpeg } from "model/photo";
|
||||
import { Photo, BatchSize, FormatJPEG } from "model/photo";
|
||||
import { ContentTypeAVC } from "common/caniuse";
|
||||
|
||||
let chai = require("chai/chai");
|
||||
let assert = chai.assert;
|
||||
|
|
@ -505,7 +506,7 @@ describe("model/photo", () => {
|
|||
FileType: "TypeJpeg",
|
||||
Width: 500,
|
||||
Height: 600,
|
||||
Hash: "1xxbgdt53",
|
||||
Hash: "ca3e60b9825bd61ee6369fcefe22f4eb92631bb5",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -536,7 +537,7 @@ describe("model/photo", () => {
|
|||
FileType: "TypeJpeg",
|
||||
Width: 500,
|
||||
Height: 600,
|
||||
Hash: "1xxbgdt53",
|
||||
Hash: "ca3e60b9825bd61ee6369fcefe22f4eb92631bb5",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -547,7 +548,7 @@ describe("model/photo", () => {
|
|||
photo.refreshFileAttr();
|
||||
assert.equal(photo.Width, 500);
|
||||
assert.equal(photo.Height, 600);
|
||||
assert.equal(photo.Hash, "1xxbgdt53");
|
||||
assert.equal(photo.Hash, "ca3e60b9825bd61ee6369fcefe22f4eb92631bb5");
|
||||
});
|
||||
|
||||
it("should return is playable", () => {
|
||||
|
|
@ -564,7 +565,7 @@ describe("model/photo", () => {
|
|||
FileType: "TypeJpeg",
|
||||
Width: 500,
|
||||
Height: 600,
|
||||
Hash: "1xxbgdt53",
|
||||
Hash: "ca3e60b9825bd61ee6369fcefe22f4eb92631bb5",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -587,7 +588,7 @@ describe("model/photo", () => {
|
|||
FileType: "mp4",
|
||||
Width: 500,
|
||||
Height: 600,
|
||||
Hash: "1xxbgdt55",
|
||||
Hash: "c1e30d265eab968155082c8e86d85815a8389479",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -607,7 +608,7 @@ describe("model/photo", () => {
|
|||
FileType: "jpg",
|
||||
Width: 500,
|
||||
Height: 600,
|
||||
Hash: "1xxbgdt53",
|
||||
Hash: "ca3e60b9825bd61ee6369fcefe22f4eb92631bb5",
|
||||
Codec: "avc1",
|
||||
},
|
||||
],
|
||||
|
|
@ -631,7 +632,7 @@ describe("model/photo", () => {
|
|||
FileType: "mp4",
|
||||
Width: 900,
|
||||
Height: 600,
|
||||
Hash: "1xxbgdt55",
|
||||
Hash: "c1e30d265eab968155082c8e86d85815a8389479",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -640,7 +641,7 @@ describe("model/photo", () => {
|
|||
assert.isAbove(result.height, 340);
|
||||
assert.isAbove(result.width, 510);
|
||||
assert.equal(result.loop, false);
|
||||
assert.equal(result.uri, "/api/v1/videos/1xxbgdt55/public/avc");
|
||||
assert.equal(result.uri, "/api/v1/videos/c1e30d265eab968155082c8e86d85815a8389479/public/avc");
|
||||
const values = {
|
||||
ID: 11,
|
||||
UID: "ABC127",
|
||||
|
|
@ -655,7 +656,7 @@ describe("model/photo", () => {
|
|||
FileType: "mp4",
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Hash: "1xxbgdt55",
|
||||
Hash: "c1e30d265eab968155082c8e86d85815a8389479",
|
||||
},
|
||||
{
|
||||
UID: "123fpp",
|
||||
|
|
@ -663,7 +664,7 @@ describe("model/photo", () => {
|
|||
Primary: true,
|
||||
Width: 5000,
|
||||
Height: 5000,
|
||||
Hash: "1xxbgdt544",
|
||||
Hash: "ca3e60b9825bd61ee6369fcefe22f4eb92631bb5",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -672,7 +673,7 @@ describe("model/photo", () => {
|
|||
assert.isAbove(result2.height, 340);
|
||||
assert.isAbove(result2.width, 340);
|
||||
assert.equal(result2.loop, false);
|
||||
assert.equal(result2.uri, "/api/v1/videos/1xxbgdt55/public/avc");
|
||||
assert.equal(result2.uri, "/api/v1/videos/c1e30d265eab968155082c8e86d85815a8389479/public/avc");
|
||||
});
|
||||
|
||||
it("should return videofile", () => {
|
||||
|
|
@ -689,7 +690,7 @@ describe("model/photo", () => {
|
|||
FileType: "jpg",
|
||||
Width: 500,
|
||||
Height: 600,
|
||||
Hash: "1xxbgdt53",
|
||||
Hash: "c1e30d265eab968155082c8e86d85815a8389479",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -711,7 +712,7 @@ describe("model/photo", () => {
|
|||
FileType: "mp4",
|
||||
Width: 500,
|
||||
Height: 600,
|
||||
Hash: "1xxbgdt55",
|
||||
Hash: "c1e30d265eab968155082c8e86d85815a8389479",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -761,13 +762,11 @@ describe("model/photo", () => {
|
|||
],
|
||||
};
|
||||
const photo = new Photo(values);
|
||||
assert.equal(photo.videoContentType(), ContentTypeAVC);
|
||||
assert.equal(photo.videoUrl(), "/api/v1/videos/703cf8f274fbb265d49c6262825780e1/public/avc");
|
||||
const values2 = { ID: 9, UID: "ABC163", Hash: "2305e512e3b183ec982d60a8b608a8ca501973ba" };
|
||||
const photo2 = new Photo(values2);
|
||||
assert.equal(
|
||||
photo2.videoUrl(),
|
||||
"/api/v1/videos/2305e512e3b183ec982d60a8b608a8ca501973ba/public/avc"
|
||||
);
|
||||
assert.equal(photo2.videoUrl(), "/api/v1/videos/2305e512e3b183ec982d60a8b608a8ca501973ba/public/avc");
|
||||
const values3 = {
|
||||
ID: 10,
|
||||
UID: "ABC127",
|
||||
|
|
@ -886,7 +885,7 @@ describe("model/photo", () => {
|
|||
UID: "123fgb",
|
||||
Name: "1980/01/superCuteKitten.jpg",
|
||||
Primary: false,
|
||||
FileType: FormatJpeg,
|
||||
FileType: FormatJPEG,
|
||||
Width: 500,
|
||||
Height: 600,
|
||||
Hash: "1xxbgdt55",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
@ -111,7 +110,7 @@ func GetVideo(router *gin.RouterGroup) {
|
|||
} else {
|
||||
// Serve embedded videos from cache to allow streaming and transcoding.
|
||||
videoBitrate = info.VideoBitrate()
|
||||
videoCodec = info.VideoCodec.String()
|
||||
videoCodec = info.VideoCodec
|
||||
videoFileType = info.VideoFileType().String()
|
||||
videoFileName = cacheName
|
||||
log.Debugf("video: streaming %s encoded %s in %s from cache", strings.ToUpper(videoCodec), strings.ToUpper(videoFileType), clean.Log(f.FileName))
|
||||
|
|
@ -119,7 +118,7 @@ func GetVideo(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Check video format support.
|
||||
supported := videoCodec != "" && videoCodec == format.Codec.String() || format.Codec == video.CodecUnknown && videoFileType == format.FileType.String()
|
||||
supported := videoCodec != "" && videoCodec == format.Codec || format.Codec == video.CodecUnknown && videoFileType == format.FileType.String()
|
||||
|
||||
// Check video bitrate against the configured limit.
|
||||
transcode := !supported || conf.FFmpegEnabled() && conf.FFmpegBitrateExceeded(videoBitrate)
|
||||
|
|
@ -153,10 +152,10 @@ func GetVideo(router *gin.RouterGroup) {
|
|||
} else {
|
||||
if videoCodec != "" && videoCodec != videoFileType {
|
||||
log.Debugf("video: %s is %s encoded and requires no transcoding, average bitrate %.1f MBit/s", clean.Log(f.FileName), strings.ToUpper(videoCodec), videoBitrate)
|
||||
AddContentTypeHeader(c, fmt.Sprintf("%s; codecs=\"%s\"", f.FileMime, clean.Codec(videoCodec)))
|
||||
AddContentTypeHeader(c, f.ContentType())
|
||||
} else {
|
||||
log.Debugf("video: %s is streamed directly, average bitrate %.1f MBit/s", clean.Log(f.FileName), videoBitrate)
|
||||
AddContentTypeHeader(c, f.FileMime)
|
||||
AddContentTypeHeader(c, f.ContentType())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,14 @@ import (
|
|||
"math"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/media/video"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
|
@ -880,3 +885,49 @@ func (m *File) SetOrientation(val int, src string) *File {
|
|||
|
||||
return m
|
||||
}
|
||||
|
||||
// ContentType returns the file content type including codec if known.
|
||||
func (m *File) ContentType() (contentType string) {
|
||||
contentType = m.FileMime
|
||||
|
||||
if m.FileVideo {
|
||||
if contentType == "" {
|
||||
var codec string
|
||||
|
||||
if m.FileCodec != "" {
|
||||
codec = m.FileCodec
|
||||
} else {
|
||||
codec = m.FileType
|
||||
}
|
||||
|
||||
switch codec {
|
||||
case "mov", "mp4", "avc", video.CodecAVC:
|
||||
contentType = header.ContentTypeAVC
|
||||
case video.CodecHEVC:
|
||||
contentType = header.ContentTypeHEVC
|
||||
case video.CodecVP8, "vp08":
|
||||
contentType = header.ContentTypeVP8
|
||||
case video.CodecVP9, "vp09":
|
||||
contentType = header.ContentTypeVP9
|
||||
case "av1", "av01":
|
||||
contentType = header.ContentTypeAV1
|
||||
case "ogg":
|
||||
contentType = header.ContentTypeOGG
|
||||
case "webm":
|
||||
contentType = header.ContentTypeWebM
|
||||
}
|
||||
}
|
||||
|
||||
if contentType != "" && !strings.Contains(contentType, ";") {
|
||||
if codec := clean.Codec(m.FileCodec); codec != "" {
|
||||
contentType = fmt.Sprintf("%s; codecs=\"%s\"", contentType, codec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentType = clean.ContentType(contentType)
|
||||
|
||||
log.Debugf("file: %s has content type %s", clean.Log(m.FileName), clean.LogQuote(contentType))
|
||||
|
||||
return contentType
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1133,7 +1133,7 @@ var FileFixtures = FileMap{
|
|||
OriginalName: "my-videos/IMG_88888.MP4",
|
||||
FileHash: "pcad9a68fa6acc5c5ba965adf6ec465ca42fd915",
|
||||
FileSize: 900,
|
||||
FileCodec: "avc1",
|
||||
FileCodec: "hvc1",
|
||||
FileType: "mp4",
|
||||
MediaType: media.Video.String(),
|
||||
FileMime: "video/mp4",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ai/face"
|
||||
|
|
@ -875,3 +877,19 @@ func TestFile_SetOrientation(t *testing.T) {
|
|||
assert.Equal(t, "", m.FileOrientationSrc)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFile_ContentType(t *testing.T) {
|
||||
t.Run("Image", func(t *testing.T) {
|
||||
m := FileFixtures.Get("exampleFileName.jpg")
|
||||
assert.Equal(t, false, m.FileVideo)
|
||||
assert.Equal(t, header.ContentTypeJPEG, m.ContentType())
|
||||
})
|
||||
t.Run("Video", func(t *testing.T) {
|
||||
avc := FileFixtures.Get("Video.mp4")
|
||||
assert.Equal(t, true, avc.FileVideo)
|
||||
assert.Equal(t, header.ContentTypeAVC, avc.ContentType())
|
||||
hevc := FileFixtures.Get("Photo21.mp4")
|
||||
assert.Equal(t, true, hevc.FileVideo)
|
||||
assert.Equal(t, header.ContentTypeHEVC, hevc.ContentType())
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ type GeoResult struct {
|
|||
FileHeight int `json:"Height" select:"files.file_height"`
|
||||
FileHash string `json:"Hash" select:"files.file_hash"`
|
||||
FileCodec string `json:"-" select:"files.file_codec"`
|
||||
FileMime string `json:"-" select:"files.file_mime"`
|
||||
FileVideo bool `json:"-" select:"files.file_video"`
|
||||
MediaType string `json:"-" select:"files.media_type"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/ulule/deepcopier"
|
||||
|
|
@ -194,28 +196,28 @@ func (m *Photo) IsPlayable() bool {
|
|||
}
|
||||
|
||||
// MediaInfo returns the media file hash and codec depending on the media type.
|
||||
func (m *Photo) MediaInfo() (mediaHash, mediaCodec string) {
|
||||
func (m *Photo) MediaInfo() (mediaHash, mediaCodec, mediaMime string) {
|
||||
if m.PhotoType == entity.MediaVideo || m.PhotoType == entity.MediaLive {
|
||||
for _, f := range m.Files {
|
||||
if f.FileVideo && f.FileHash != "" {
|
||||
return f.FileHash, f.FileCodec
|
||||
return f.FileHash, f.FileCodec, clean.ContentType(f.FileMime)
|
||||
}
|
||||
}
|
||||
} else if m.PhotoType == entity.MediaVector {
|
||||
for _, f := range m.Files {
|
||||
if f.MediaType == entity.MediaVector && f.FileHash != "" {
|
||||
return f.FileHash, f.FileCodec
|
||||
return f.FileHash, f.FileCodec, clean.ContentType(f.FileMime)
|
||||
}
|
||||
}
|
||||
} else if m.PhotoType == entity.MediaDocument {
|
||||
for _, f := range m.Files {
|
||||
if f.MediaType == entity.MediaDocument && f.FileHash != "" {
|
||||
return f.FileHash, f.FileCodec
|
||||
return f.FileHash, f.FileCodec, clean.ContentType(f.FileMime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m.FileHash, ""
|
||||
return m.FileHash, "", m.FileMime
|
||||
}
|
||||
|
||||
// ShareBase returns a meaningful file name for sharing.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/media/video"
|
||||
|
|
@ -180,7 +182,8 @@ func TestPhoto_MediaInfo(t *testing.T) {
|
|||
{
|
||||
FileVideo: true,
|
||||
MediaType: media.Video.String(),
|
||||
FileCodec: video.CodecAVC.String(),
|
||||
FileMime: header.ContentTypeAVC,
|
||||
FileCodec: video.CodecAVC,
|
||||
FileHash: "53c89dcfa006c9e592dd9e6db4b31cd57be64b81",
|
||||
},
|
||||
},
|
||||
|
|
@ -188,9 +191,10 @@ func TestPhoto_MediaInfo(t *testing.T) {
|
|||
|
||||
assert.True(t, r.IsPlayable())
|
||||
|
||||
mediaHash, mediaCodec := r.MediaInfo()
|
||||
mediaHash, mediaCodec, mediaMime := r.MediaInfo()
|
||||
assert.Equal(t, "53c89dcfa006c9e592dd9e6db4b31cd57be64b81", mediaHash)
|
||||
assert.Equal(t, video.CodecAVC.String(), mediaCodec)
|
||||
assert.Equal(t, video.CodecAVC, mediaCodec)
|
||||
assert.Equal(t, header.ContentTypeAVC, mediaMime)
|
||||
})
|
||||
t.Run("VideoCodecHVC", func(t *testing.T) {
|
||||
r := Photo{
|
||||
|
|
@ -207,24 +211,28 @@ func TestPhoto_MediaInfo(t *testing.T) {
|
|||
{
|
||||
FileVideo: false,
|
||||
MediaType: media.Image.String(),
|
||||
FileMime: header.ContentTypeJPEG,
|
||||
FileCodec: "jpeg",
|
||||
},
|
||||
{
|
||||
FileVideo: true,
|
||||
MediaType: media.Video.String(),
|
||||
FileMime: header.ContentTypeAVC,
|
||||
FileCodec: "xyz",
|
||||
FileHash: "",
|
||||
},
|
||||
{
|
||||
FileVideo: true,
|
||||
MediaType: media.Video.String(),
|
||||
FileCodec: video.CodecHVC.String(),
|
||||
FileCodec: video.CodecHEVC,
|
||||
FileMime: header.ContentTypeHEVC,
|
||||
FileHash: "057258b0c88c2e017ec171cc8799a5df7badbadf",
|
||||
},
|
||||
{
|
||||
FileVideo: true,
|
||||
MediaType: media.Video.String(),
|
||||
FileCodec: video.CodecAVC.String(),
|
||||
FileCodec: video.CodecAVC,
|
||||
FileMime: header.ContentTypeAVC,
|
||||
FileHash: "ddb3f44eb500d7669cbe0a95e66d5a63f642487d",
|
||||
},
|
||||
},
|
||||
|
|
@ -232,9 +240,10 @@ func TestPhoto_MediaInfo(t *testing.T) {
|
|||
|
||||
assert.True(t, r.IsPlayable())
|
||||
|
||||
mediaHash, mediaCodec := r.MediaInfo()
|
||||
mediaHash, mediaCodec, mediaMime := r.MediaInfo()
|
||||
assert.Equal(t, "057258b0c88c2e017ec171cc8799a5df7badbadf", mediaHash)
|
||||
assert.Equal(t, video.CodecHVC.String(), mediaCodec)
|
||||
assert.Equal(t, video.CodecHEVC, mediaCodec)
|
||||
assert.Equal(t, header.ContentTypeHEVC, mediaMime)
|
||||
})
|
||||
t.Run("NoVideoHash", func(t *testing.T) {
|
||||
r := Photo{
|
||||
|
|
@ -251,6 +260,7 @@ func TestPhoto_MediaInfo(t *testing.T) {
|
|||
{
|
||||
FileVideo: true,
|
||||
MediaType: media.Video.String(),
|
||||
FileMime: header.ContentTypeAVC,
|
||||
FileHash: "",
|
||||
},
|
||||
},
|
||||
|
|
@ -258,9 +268,10 @@ func TestPhoto_MediaInfo(t *testing.T) {
|
|||
|
||||
assert.True(t, r.IsPlayable())
|
||||
|
||||
mediaHash, mediaCodec := r.MediaInfo()
|
||||
mediaHash, mediaCodec, mediaMime := r.MediaInfo()
|
||||
assert.Equal(t, "e22a06fb5b63dae7f3d08ab95fb958935b744e51", mediaHash)
|
||||
assert.Equal(t, "", mediaCodec)
|
||||
assert.Equal(t, "", mediaMime)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func UserPhotosViewerResults(frm form.SearchPhotos, sess *entity.Session, conten
|
|||
|
||||
// ViewerResult returns a new photo viewer result.
|
||||
func (m *Photo) ViewerResult(contentUri, apiUri, previewToken, downloadToken string) viewer.Result {
|
||||
mediaHash, mediaCodec := m.MediaInfo()
|
||||
mediaHash, mediaCodec, mediaMime := m.MediaInfo()
|
||||
return viewer.Result{
|
||||
UID: m.PhotoUID,
|
||||
Type: m.PhotoType,
|
||||
|
|
@ -41,6 +41,7 @@ func (m *Photo) ViewerResult(contentUri, apiUri, previewToken, downloadToken str
|
|||
Height: m.FileHeight,
|
||||
Hash: mediaHash,
|
||||
Codec: mediaCodec,
|
||||
Mime: mediaMime,
|
||||
Thumbs: thumb.ViewerThumbs(m.FileWidth, m.FileHeight, m.FileHash, contentUri, previewToken),
|
||||
DownloadUrl: viewer.DownloadUrl(m.FileHash, apiUri, downloadToken),
|
||||
}
|
||||
|
|
@ -79,6 +80,7 @@ func (m GeoResult) ViewerResult(contentUri, apiUri, previewToken, downloadToken
|
|||
Height: m.FileHeight,
|
||||
Hash: m.FileHash,
|
||||
Codec: m.FileCodec,
|
||||
Mime: m.FileMime,
|
||||
Thumbs: thumb.ViewerThumbs(m.FileWidth, m.FileHeight, m.FileHash, contentUri, previewToken),
|
||||
DownloadUrl: viewer.DownloadUrl(m.FileHash, apiUri, downloadToken),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type Result struct {
|
|||
Height int `json:"Height"`
|
||||
Hash string `json:"Hash"`
|
||||
Codec string `json:"Codec,omitempty"`
|
||||
Mime string `json:"Mime,omitempty"`
|
||||
Thumbs *thumb.Viewer `json:"Thumbs"`
|
||||
DownloadUrl string `json:"DownloadUrl,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, "earth.ogv", data.FileName)
|
||||
assert.Equal(t, string(video.CodecOGV), data.Codec)
|
||||
assert.Equal(t, video.CodecOGV, data.Codec)
|
||||
assert.Equal(t, "0s", data.Duration.String())
|
||||
assert.Equal(t, 1280, data.Width)
|
||||
assert.Equal(t, 720, data.Height)
|
||||
|
|
@ -128,7 +128,7 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, "earth.vp8.webm", data.FileName)
|
||||
assert.Equal(t, string(video.CodecVP8), data.Codec)
|
||||
assert.Equal(t, video.CodecVP8, data.Codec)
|
||||
assert.Equal(t, "30s", data.Duration.String())
|
||||
assert.Equal(t, 1920, data.Width)
|
||||
assert.Equal(t, 1080, data.Height)
|
||||
|
|
@ -144,7 +144,7 @@ func TestJSON(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.Equal(t, "earth-animation.ogv.720p.vp9.webm", data.FileName)
|
||||
assert.Equal(t, string(video.CodecVP9), data.Codec)
|
||||
assert.Equal(t, video.CodecVP9, data.Codec)
|
||||
assert.Equal(t, "8.03s", data.Duration.String())
|
||||
assert.Equal(t, 1280, data.Width)
|
||||
assert.Equal(t, 720, data.Height)
|
||||
|
|
@ -705,7 +705,7 @@ func TestJSON(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecAVC), data.Codec)
|
||||
assert.Equal(t, video.CodecAVC, data.Codec)
|
||||
assert.Equal(t, "10.67s", data.Duration.String())
|
||||
assert.Equal(t, "2015-12-06 18:22:29 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2015-12-06 15:22:29 +0000 UTC", data.TakenAt.String())
|
||||
|
|
@ -730,7 +730,7 @@ func TestJSON(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecHVC), data.Codec)
|
||||
assert.Equal(t, video.CodecHEVC, data.Codec)
|
||||
assert.Equal(t, "6.83s", data.Duration.String())
|
||||
assert.Equal(t, "2020-12-22 02:45:43 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2020-12-22 01:45:43 +0000 UTC", data.TakenAt.String())
|
||||
|
|
@ -754,7 +754,7 @@ func TestJSON(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecHVC), data.Codec)
|
||||
assert.Equal(t, video.CodecHEVC, data.Codec)
|
||||
assert.Equal(t, "2.15s", data.Duration.String())
|
||||
assert.Equal(t, "2019-12-12 20:47:21 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2019-12-13 01:47:21 +0000 UTC", data.TakenAt.String())
|
||||
|
|
@ -792,7 +792,7 @@ func TestJSON(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecAVC), data.Codec)
|
||||
assert.Equal(t, video.CodecAVC, data.Codec)
|
||||
assert.Equal(t, "6.09s", data.Duration.String())
|
||||
assert.Equal(t, "2022-06-25 06:50:58 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2022-06-25 04:50:58 +0000 UTC", data.TakenAt.String())
|
||||
|
|
@ -969,7 +969,7 @@ func TestJSON(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecAVC), data.Codec)
|
||||
assert.Equal(t, video.CodecAVC, data.Codec)
|
||||
assert.Equal(t, "1.03s", data.Duration.String())
|
||||
assert.Equal(t, "2012-07-11 07:16:01 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2012-07-11 05:16:01 +0000 UTC", data.TakenAt.String())
|
||||
|
|
@ -986,7 +986,7 @@ func TestJSON(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecAVC), data.Codec)
|
||||
assert.Equal(t, video.CodecAVC, data.Codec)
|
||||
assert.Equal(t, "1.03s", data.Duration.String())
|
||||
assert.Equal(t, "2012-07-11 07:16:01 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2012-07-11 05:16:01 +0000 UTC", data.TakenAt.String())
|
||||
|
|
@ -1003,7 +1003,7 @@ func TestJSON(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecAVC), data.Codec)
|
||||
assert.Equal(t, video.CodecAVC, data.Codec)
|
||||
assert.Equal(t, "1.03s", data.Duration.String())
|
||||
assert.Equal(t, "2012-07-11 07:16:01 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2012-07-11 05:16:01 +0000 UTC", data.TakenAt.String())
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@ import (
|
|||
)
|
||||
|
||||
const CodecUnknown = ""
|
||||
const CodecAv1 = string(video.CodecAV1)
|
||||
const CodecAvc1 = string(video.CodecAVC)
|
||||
const CodecHvc1 = string(video.CodecHVC)
|
||||
const CodecAv1 = video.CodecAV1
|
||||
const CodecAvc1 = video.CodecAVC
|
||||
const CodecHvc1 = video.CodecHEVC
|
||||
const CodecJpeg = "jpeg"
|
||||
const CodecHeic = "heic"
|
||||
const CodecXMP = "xmp"
|
||||
|
|
|
|||
|
|
@ -811,7 +811,7 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
|||
// Update file properties.
|
||||
file.FileSidecar = m.IsSidecar()
|
||||
file.FileType = m.FileType().String()
|
||||
file.FileMime = m.MimeType()
|
||||
file.FileMime = m.ContentType()
|
||||
file.SetOrientation(m.Orientation(), entity.SrcMeta)
|
||||
file.ModTime = modTime.UTC().Truncate(time.Second).Unix()
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ type MediaFile struct {
|
|||
fileSize int64
|
||||
fileType fs.Type
|
||||
mimeType string
|
||||
contentType string
|
||||
takenAt time.Time
|
||||
takenAtSrc string
|
||||
hash string
|
||||
|
|
@ -508,7 +509,11 @@ func (m *MediaFile) Root() string {
|
|||
return m.fileRoot
|
||||
}
|
||||
|
||||
// MimeType returns the mime type.
|
||||
// MimeType returns the mimetype of this file, or an empty string if it could not be determined.
|
||||
//
|
||||
// The IANA and IETF use the term "media type", and consider the term "MIME type" to be obsolete,
|
||||
// since media types have become used in contexts unrelated to email, such as HTTP:
|
||||
// https://en.wikipedia.org/wiki/Media_type#Structure
|
||||
func (m *MediaFile) MimeType() string {
|
||||
if m.mimeType != "" {
|
||||
return m.mimeType
|
||||
|
|
@ -527,6 +532,39 @@ func (m *MediaFile) MimeType() string {
|
|||
return m.mimeType
|
||||
}
|
||||
|
||||
// BaseType returns the basic mime type, without any optional parameters.
|
||||
func (m *MediaFile) BaseType() string {
|
||||
return fs.BaseType(m.MimeType())
|
||||
}
|
||||
|
||||
// HasMimeType tests if the specified mime type is the same, except for any optional parameters.
|
||||
func (m *MediaFile) HasMimeType(mimeType string) bool {
|
||||
return fs.IsType(m.MimeType(), mimeType)
|
||||
}
|
||||
|
||||
// ContentType returns the media content type.
|
||||
func (m *MediaFile) ContentType() string {
|
||||
if m.contentType != "" {
|
||||
return m.contentType
|
||||
}
|
||||
|
||||
m.contentType = m.MimeType()
|
||||
|
||||
// Append codecs if media file is a video.
|
||||
if m.IsVideo() {
|
||||
if !strings.Contains(m.contentType, ";") && m.MetaData().Codec != "" {
|
||||
m.contentType = fmt.Sprintf("%s; codecs=\"%s\"", m.contentType, clean.Codec(m.MetaData().Codec))
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize media content type.
|
||||
m.contentType = clean.ContentType(m.contentType)
|
||||
|
||||
log.Debugf("media: %s has content type %s", clean.Log(m.RootRelName()), clean.LogQuote(m.contentType))
|
||||
|
||||
return m.contentType
|
||||
}
|
||||
|
||||
// openFile opens the file and returns the descriptor.
|
||||
func (m *MediaFile) openFile() (handle *os.File, err error) {
|
||||
fileName := m.FileName()
|
||||
|
|
@ -688,7 +726,7 @@ func (m *MediaFile) IsJpeg() bool {
|
|||
}
|
||||
|
||||
// Check the mime type after other tests have passed to improve performance.
|
||||
return m.MimeType() == fs.MimeTypeJPEG
|
||||
return m.HasMimeType(fs.MimeTypeJPEG)
|
||||
}
|
||||
|
||||
// IsJpegXL checks if the file is a JPEG XL image with a supported file type extension.
|
||||
|
|
@ -698,7 +736,7 @@ func (m *MediaFile) IsJpegXL() bool {
|
|||
}
|
||||
|
||||
// Check the mime type after other tests have passed to improve performance.
|
||||
return m.MimeType() == fs.MimeTypeJPEGXL
|
||||
return m.HasMimeType(fs.MimeTypeJPEGXL)
|
||||
}
|
||||
|
||||
// IsPNG checks if the file is a PNG image with a supported file type extension.
|
||||
|
|
@ -710,8 +748,7 @@ func (m *MediaFile) IsPNG() bool {
|
|||
}
|
||||
|
||||
// Check the mime type after other tests have passed to improve performance.
|
||||
mimeType := m.MimeType()
|
||||
return mimeType == fs.MimeTypePNG || mimeType == fs.MimeTypeAPNG
|
||||
return m.HasMimeType(fs.MimeTypePNG) || m.HasMimeType(fs.MimeTypeAPNG)
|
||||
}
|
||||
|
||||
// IsGIF checks if the file is a GIF image with a supported file type extension.
|
||||
|
|
@ -721,7 +758,7 @@ func (m *MediaFile) IsGIF() bool {
|
|||
}
|
||||
|
||||
// Check the mime type after other tests have passed to improve performance.
|
||||
return m.MimeType() == fs.MimeTypeGIF
|
||||
return m.HasMimeType(fs.MimeTypeGIF)
|
||||
}
|
||||
|
||||
// IsTIFF checks if the file is a TIFF image with a supported file type extension.
|
||||
|
|
@ -731,7 +768,7 @@ func (m *MediaFile) IsTIFF() bool {
|
|||
}
|
||||
|
||||
// Check the mime type after other tests have passed to improve performance.
|
||||
return m.MimeType() == fs.MimeTypeTIFF
|
||||
return m.HasMimeType(fs.MimeTypeTIFF)
|
||||
}
|
||||
|
||||
// IsDNG checks if the file is a Adobe Digital Negative (DNG) image with a supported file type extension.
|
||||
|
|
@ -740,7 +777,7 @@ func (m *MediaFile) IsDNG() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
return m.MimeType() == fs.MimeTypeDNG
|
||||
return m.HasMimeType(fs.MimeTypeDNG)
|
||||
}
|
||||
|
||||
// IsHEIF checks if the file is a High Efficiency Image File Format (HEIF) container with a supported file type extension.
|
||||
|
|
@ -755,8 +792,7 @@ func (m *MediaFile) IsHEIC() bool {
|
|||
}
|
||||
|
||||
// Check the mime type after other tests have passed to improve performance.
|
||||
mimeType := m.MimeType()
|
||||
return mimeType == fs.MimeTypeHEIC || mimeType == fs.MimeTypeHEICS
|
||||
return m.HasMimeType(fs.MimeTypeHEIC) || m.HasMimeType(fs.MimeTypeHEICS)
|
||||
}
|
||||
|
||||
// IsHEICS checks if the file is a HEIC image sequence with a supported file type extension.
|
||||
|
|
@ -770,7 +806,7 @@ func (m *MediaFile) IsAVIF() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
return m.MimeType() == fs.MimeTypeAVIF
|
||||
return m.HasMimeType(fs.MimeTypeAVIF)
|
||||
}
|
||||
|
||||
// IsAVIFS checks if the file is an AVIF image sequence with a supported file type extension.
|
||||
|
|
@ -785,7 +821,7 @@ func (m *MediaFile) IsBMP() bool {
|
|||
}
|
||||
|
||||
// Check the mime type after other tests have passed to improve performance.
|
||||
return m.MimeType() == fs.MimeTypeBMP
|
||||
return m.HasMimeType(fs.MimeTypeBMP)
|
||||
}
|
||||
|
||||
// IsWebP checks if the file is a WebP image file with a supported file type extension.
|
||||
|
|
@ -794,7 +830,7 @@ func (m *MediaFile) IsWebP() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
return m.MimeType() == fs.MimeTypeWebP
|
||||
return m.HasMimeType(fs.MimeTypeWebP)
|
||||
}
|
||||
|
||||
// Duration returns the duration is the media content is playable.
|
||||
|
|
@ -853,7 +889,7 @@ func (m *MediaFile) CheckType() error {
|
|||
|
||||
// Detect media type (formerly known as a MIME type),
|
||||
// see https://en.wikipedia.org/wiki/Media_type
|
||||
mimeType := m.MimeType()
|
||||
mimeType := m.BaseType()
|
||||
|
||||
// Perform mime type checks for selected file types.
|
||||
var valid bool
|
||||
|
|
|
|||
|
|
@ -108,13 +108,13 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
|
|||
}
|
||||
|
||||
if len(result.Files) == 0 || result.Main == nil {
|
||||
t := m.MimeType()
|
||||
mediaType := m.BaseType()
|
||||
|
||||
if t == "" {
|
||||
t = "unknown type"
|
||||
if mediaType == "" {
|
||||
mediaType = "unknown type"
|
||||
}
|
||||
|
||||
return result, fmt.Errorf("%s is unsupported (%s)", clean.Log(m.BaseName()), t)
|
||||
return result, fmt.Errorf("%s is unsupported (%s)", clean.Log(m.BaseName()), mediaType)
|
||||
}
|
||||
|
||||
// Add hidden preview image if needed.
|
||||
|
|
|
|||
|
|
@ -699,10 +699,15 @@ func TestMediaFile_MimeType(t *testing.T) {
|
|||
})
|
||||
t.Run("iphone_7.xmp", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.xmp")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "text/plain", mediaFile.MimeType())
|
||||
|
||||
assert.Equal(t, fs.MimeTypeText, mediaFile.BaseType())
|
||||
assert.Equal(t, "text/plain", mediaFile.BaseType())
|
||||
assert.Equal(t, "text/plain; charset=utf-8", mediaFile.MimeType())
|
||||
assert.Equal(t, true, mediaFile.HasMimeType("text/plain"))
|
||||
})
|
||||
t.Run("iphone_7.json", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.json")
|
||||
|
|
@ -732,7 +737,11 @@ func TestMediaFile_MimeType(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, fs.MimeTypeXML, mediaFile.MimeType())
|
||||
|
||||
assert.Equal(t, fs.MimeTypeXML, mediaFile.BaseType())
|
||||
assert.Equal(t, "text/xml", mediaFile.BaseType())
|
||||
assert.Equal(t, "text/xml; charset=utf-8", mediaFile.MimeType())
|
||||
assert.Equal(t, true, mediaFile.HasMimeType("text/xml"))
|
||||
})
|
||||
t.Run("earth.mov", func(t *testing.T) {
|
||||
if f, err := NewMediaFile(filepath.Join(c.ExamplesPath(), "earth.mov")); err != nil {
|
||||
|
|
@ -2023,10 +2032,11 @@ func TestMediaFile_ExceedsResolution(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMediaFile_AspectRatio(t *testing.T) {
|
||||
t.Run("iphone_7.heic", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
c := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
|
||||
t.Run("iphone_7.heic", func(t *testing.T) {
|
||||
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.heic")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -2037,9 +2047,7 @@ func TestMediaFile_AspectRatio(t *testing.T) {
|
|||
assert.False(t, mediaFile.Square())
|
||||
})
|
||||
t.Run("fern_green.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/fern_green.jpg")
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/fern_green.jpg")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -2050,9 +2058,7 @@ func TestMediaFile_AspectRatio(t *testing.T) {
|
|||
assert.True(t, mediaFile.Square())
|
||||
})
|
||||
t.Run("elephants.jpg", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg")
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/elephants.jpg")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -2116,6 +2122,8 @@ func TestMediaFile_FileType(t *testing.T) {
|
|||
assert.False(t, m.IsPNG())
|
||||
assert.Equal(t, "png", string(m.FileType()))
|
||||
assert.Equal(t, "image/jpeg", m.MimeType())
|
||||
assert.Equal(t, "image/jpeg", m.BaseType())
|
||||
assert.Equal(t, true, m.HasMimeType("image/jpeg"))
|
||||
assert.Equal(t, fs.ImagePNG, m.FileType())
|
||||
assert.Equal(t, ".png", m.Extension())
|
||||
})
|
||||
|
|
@ -2131,6 +2139,7 @@ func TestMediaFile_FileType(t *testing.T) {
|
|||
assert.False(t, m.IsPNG())
|
||||
assert.Equal(t, "thm", string(m.FileType()))
|
||||
assert.Equal(t, "image/jpeg", m.MimeType())
|
||||
assert.Equal(t, true, m.HasMimeType("image/jpeg"))
|
||||
assert.Equal(t, fs.ImageThumb, m.FileType())
|
||||
assert.Equal(t, ".thm", m.Extension())
|
||||
})
|
||||
|
|
@ -2138,9 +2147,9 @@ func TestMediaFile_FileType(t *testing.T) {
|
|||
|
||||
func TestMediaFile_Stat(t *testing.T) {
|
||||
t.Run("iphone_7.heic", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
c := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.heic")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -2159,9 +2168,9 @@ func TestMediaFile_Stat(t *testing.T) {
|
|||
|
||||
func TestMediaFile_FileSize(t *testing.T) {
|
||||
t.Run("iphone_7.heic", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
c := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.heic")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -2174,9 +2183,9 @@ func TestMediaFile_FileSize(t *testing.T) {
|
|||
|
||||
func TestMediaFile_JsonName(t *testing.T) {
|
||||
t.Run("blue-go-video.mp4", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
c := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/blue-go-video.mp4")
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/blue-go-video.mp4")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -2188,10 +2197,10 @@ func TestMediaFile_JsonName(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMediaFile_PathNameInfo(t *testing.T) {
|
||||
t.Run("blue-go-video.mp4", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
c := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/blue-go-video.mp4")
|
||||
t.Run("blue-go-video.mp4", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/blue-go-video.mp4")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -2204,11 +2213,8 @@ func TestMediaFile_PathNameInfo(t *testing.T) {
|
|||
assert.Equal(t, "blue-go-video.mp4", name)
|
||||
|
||||
})
|
||||
|
||||
t.Run("beach_sand sidecar", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/beach_sand.jpg")
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/beach_sand.jpg")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -2224,13 +2230,11 @@ func TestMediaFile_PathNameInfo(t *testing.T) {
|
|||
assert.Equal(t, ".photoprism/beach_sand.jpg", name)
|
||||
mediaFile.SetFileName(initialName)
|
||||
})
|
||||
|
||||
t.Run("beach_sand import", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
t.Log(Config().SidecarPath())
|
||||
t.Log(Config().ImportPath())
|
||||
|
||||
mediaFile, err := NewMediaFile(filepath.Join(conf.ExamplesPath(), "beach_sand.jpg"))
|
||||
mediaFile, err := NewMediaFile(filepath.Join(c.ExamplesPath(), "beach_sand.jpg"))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -2238,7 +2242,7 @@ func TestMediaFile_PathNameInfo(t *testing.T) {
|
|||
|
||||
initialName := mediaFile.FileName()
|
||||
t.Log(initialName)
|
||||
mediaFile.SetFileName(filepath.Join(conf.ImportPath(), "beach_sand.jpg"))
|
||||
mediaFile.SetFileName(filepath.Join(c.ImportPath(), "beach_sand.jpg"))
|
||||
|
||||
root, base, path, name := mediaFile.PathNameInfo(true)
|
||||
assert.Equal(t, "import", root)
|
||||
|
|
@ -2249,9 +2253,7 @@ func TestMediaFile_PathNameInfo(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("beach_sand unknown root", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/beach_sand.jpg")
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/beach_sand.jpg")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -2270,10 +2272,10 @@ func TestMediaFile_PathNameInfo(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMediaFile_SubDirectory(t *testing.T) {
|
||||
t.Run("blue-go-video.mp4", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
c := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/blue-go-video.mp4")
|
||||
t.Run("blue-go-video.mp4", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/blue-go-video.mp4")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
@ -2285,16 +2287,16 @@ func TestMediaFile_SubDirectory(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMediaFile_HasSameName(t *testing.T) {
|
||||
t.Run("false", func(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
c := config.TestConfig()
|
||||
|
||||
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/blue-go-video.mp4")
|
||||
t.Run("false", func(t *testing.T) {
|
||||
mediaFile, err := NewMediaFile(c.ExamplesPath() + "/blue-go-video.mp4")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mediaFile2, err := NewMediaFile(conf.ExamplesPath() + "/beach_sand.jpg")
|
||||
mediaFile2, err := NewMediaFile(c.ExamplesPath() + "/beach_sand.jpg")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
|||
39
pkg/clean/content_type.go
Normal file
39
pkg/clean/content_type.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package clean
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
)
|
||||
|
||||
// ContentType normalizes media content type strings, see https://en.wikipedia.org/wiki/Media_type.
|
||||
func ContentType(s string) string {
|
||||
if s == "" {
|
||||
return header.ContentTypeBinary
|
||||
}
|
||||
|
||||
s = Type(s)
|
||||
|
||||
switch s {
|
||||
case "":
|
||||
return header.ContentTypeBinary
|
||||
case "text/json", "application/json":
|
||||
return header.ContentTypeJsonUtf8
|
||||
case "text/htm", "text/html":
|
||||
return header.ContentTypeHtml
|
||||
case "text/plain":
|
||||
return header.ContentTypeText
|
||||
case "text/pdf", "text/x-pdf", "application/x-pdf", "application/acrobat":
|
||||
return header.ContentTypePDF
|
||||
case "image/svg":
|
||||
return header.ContentTypeSVG
|
||||
case "image/jpe", "image/jpg":
|
||||
return header.ContentTypeJPEG
|
||||
case "video/mp4; codecs=\"avc\"":
|
||||
return header.ContentTypeAVC
|
||||
case "video/mp4; codecs=\"hvc1\"", "video/mp4; codecs=\"hvc\"", "video/mp4; codecs=\"hevc\"":
|
||||
return header.ContentTypeHEVC
|
||||
case "video/webm; codecs=\"av01\"":
|
||||
return header.ContentTypeAV1
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
|
@ -31,8 +31,10 @@ func Log(s string) string {
|
|||
case ' ':
|
||||
spaces = true
|
||||
return r
|
||||
case '`', '"':
|
||||
case '`':
|
||||
return '\''
|
||||
case '"':
|
||||
return '"'
|
||||
case '\\', '$', '<', '>', '{', '}':
|
||||
return '?'
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ func TestLog(t *testing.T) {
|
|||
assert.Equal(t, "?", Log("User-Agent: {jndi:ldap://<host>:<port>/<path>}"))
|
||||
})
|
||||
t.Run("SpecialChars", func(t *testing.T) {
|
||||
assert.Equal(t, "' The ?quick? ''brown 'fox. '", Log(" The <quick>\n\r ''brown \"fox. \t "))
|
||||
assert.Equal(t, "' The ?quick? ''brown \"fox. '", Log(" The <quick>\n\r ''brown \"fox. \t "))
|
||||
})
|
||||
t.Run("LoremIpsum", func(t *testing.T) {
|
||||
assert.Equal(t, "'It is a long established fact that a reader will be distracted by the readable "+
|
||||
|
|
|
|||
|
|
@ -29,11 +29,16 @@ const (
|
|||
MimeTypeAI = "application/vnd.adobe.illustrator"
|
||||
MimeTypePS = "application/postscript"
|
||||
MimeTypeEPS = "image/eps"
|
||||
MimeTypeText = "text/plain"
|
||||
MimeTypeXML = "text/xml"
|
||||
MimeTypeJSON = "application/json"
|
||||
)
|
||||
|
||||
// MimeType returns the mime type of a file, or an empty string if it could not be detected.
|
||||
// MimeType returns the mimetype of a file, or an empty string if it could not be determined.
|
||||
//
|
||||
// The IANA and IETF use the term "media type", and consider the term "MIME type" to be obsolete,
|
||||
// since media types have become used in contexts unrelated to email, such as HTTP:
|
||||
// https://en.wikipedia.org/wiki/Media_type#Structure
|
||||
func MimeType(filename string) (mimeType string) {
|
||||
if filename == "" {
|
||||
return MimeTypeUnknown
|
||||
|
|
@ -69,7 +74,7 @@ func MimeType(filename string) (mimeType string) {
|
|||
detectedType, err := mimetype.DetectFile(filename)
|
||||
|
||||
if detectedType != nil && err == nil {
|
||||
mimeType, _, _ = strings.Cut(detectedType.String(), ";")
|
||||
mimeType = detectedType.String()
|
||||
}
|
||||
|
||||
// Treat "application/octet-stream" as unknown.
|
||||
|
|
@ -100,3 +105,23 @@ func MimeType(filename string) (mimeType string) {
|
|||
|
||||
return mimeType
|
||||
}
|
||||
|
||||
// BaseType returns the media type string without any optional parameters.
|
||||
func BaseType(mimeType string) string {
|
||||
if mimeType == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
mimeType, _, _ = strings.Cut(mimeType, ";")
|
||||
|
||||
return strings.ToLower(mimeType)
|
||||
}
|
||||
|
||||
// IsType tests if the specified mime types are matching, except for any optional parameters.
|
||||
func IsType(mime1, mime2 string) bool {
|
||||
if mime1 == mime2 {
|
||||
return true
|
||||
}
|
||||
|
||||
return BaseType(mime1) == BaseType(mime2)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,3 +77,93 @@ func TestMimeType(t *testing.T) {
|
|||
assert.Equal(t, "image/eps", mimeType)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBaseType(t *testing.T) {
|
||||
t.Run("MP4", func(t *testing.T) {
|
||||
filename := Abs("./testdata/test.mp4")
|
||||
mimeType := BaseType(MimeType(filename))
|
||||
assert.Equal(t, "video/mp4", mimeType)
|
||||
})
|
||||
t.Run("MOV", func(t *testing.T) {
|
||||
filename := Abs("./testdata/test.mov")
|
||||
mimeType := BaseType(MimeType(filename))
|
||||
assert.Equal(t, "video/quicktime", mimeType)
|
||||
})
|
||||
t.Run("JPEG", func(t *testing.T) {
|
||||
filename := Abs("./testdata/test.jpg")
|
||||
mimeType := BaseType(MimeType(filename))
|
||||
assert.Equal(t, "image/jpeg", mimeType)
|
||||
})
|
||||
t.Run("InvalidFilename", func(t *testing.T) {
|
||||
filename := Abs("./testdata/xxx.jpg")
|
||||
mimeType := BaseType(MimeType(filename))
|
||||
assert.Equal(t, "", mimeType)
|
||||
})
|
||||
t.Run("EmptyFilename", func(t *testing.T) {
|
||||
mimeType := BaseType("")
|
||||
assert.Equal(t, "", mimeType)
|
||||
})
|
||||
t.Run("AVIF", func(t *testing.T) {
|
||||
filename := Abs("./testdata/test.avif")
|
||||
mimeType := BaseType(MimeType(filename))
|
||||
assert.Equal(t, "image/avif", mimeType)
|
||||
})
|
||||
t.Run("AVIFS", func(t *testing.T) {
|
||||
filename := Abs("./testdata/test.avifs")
|
||||
mimeType := MimeType(filename)
|
||||
assert.Equal(t, "image/avif-sequence", mimeType)
|
||||
})
|
||||
t.Run("HEIC", func(t *testing.T) {
|
||||
filename := Abs("./testdata/test.heic")
|
||||
mimeType := BaseType(MimeType(filename))
|
||||
assert.Equal(t, "image/heic", mimeType)
|
||||
})
|
||||
t.Run("HEICS", func(t *testing.T) {
|
||||
filename := Abs("./testdata/test.heics")
|
||||
mimeType := BaseType(MimeType(filename))
|
||||
assert.Equal(t, "image/heic-sequence", mimeType)
|
||||
})
|
||||
t.Run("DNG", func(t *testing.T) {
|
||||
filename := Abs("./testdata/test.dng")
|
||||
mimeType := BaseType(MimeType(filename))
|
||||
assert.Equal(t, "image/dng", mimeType)
|
||||
})
|
||||
t.Run("SVG", func(t *testing.T) {
|
||||
filename := Abs("./testdata/test.svg")
|
||||
mimeType := BaseType(MimeType(filename))
|
||||
assert.Equal(t, "image/svg+xml", mimeType)
|
||||
})
|
||||
t.Run("AI", func(t *testing.T) {
|
||||
filename := Abs("./testdata/test.ai")
|
||||
mimeType := BaseType(MimeType(filename))
|
||||
assert.Equal(t, "application/vnd.adobe.illustrator", mimeType)
|
||||
})
|
||||
t.Run("PS", func(t *testing.T) {
|
||||
filename := Abs("./testdata/test.ps")
|
||||
mimeType := BaseType(MimeType(filename))
|
||||
assert.Equal(t, "application/postscript", mimeType)
|
||||
})
|
||||
t.Run("EPS", func(t *testing.T) {
|
||||
filename := Abs("./testdata/test.eps")
|
||||
mimeType := BaseType(MimeType(filename))
|
||||
assert.Equal(t, "image/eps", mimeType)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsType(t *testing.T) {
|
||||
t.Run("True", func(t *testing.T) {
|
||||
assert.True(t, IsType("", MimeTypeUnknown))
|
||||
assert.True(t, IsType("video/jpg", "video/jpg"))
|
||||
assert.True(t, IsType("video/jpeg", "video/jpeg"))
|
||||
assert.True(t, IsType("video/mp4", "video/mp4"))
|
||||
assert.True(t, IsType("video/mp4", MimeTypeMP4))
|
||||
assert.True(t, IsType("video/mp4", "video/MP4"))
|
||||
assert.True(t, IsType("video/mp4", "video/MP4; codecs=\"avc1\""))
|
||||
})
|
||||
t.Run("False", func(t *testing.T) {
|
||||
assert.False(t, IsType("", MimeTypeMP4))
|
||||
assert.False(t, IsType("video/jpeg", "video/jpg"))
|
||||
assert.False(t, IsType("video/mp4", MimeTypeUnknown))
|
||||
assert.False(t, IsType(MimeTypeMP4, MimeTypeJPEG))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,22 @@ const (
|
|||
|
||||
// Standard ContentType header values.
|
||||
const (
|
||||
ContentTypeBinary = "application/octet-stream"
|
||||
ContentTypeForm = "application/x-www-form-urlencoded"
|
||||
ContentTypeMultipart = "multipart/form-data"
|
||||
ContentTypeJson = "application/json"
|
||||
ContentTypeJsonUtf8 = "application/json; charset=utf-8"
|
||||
ContentTypeHtml = "text/html; charset=utf-8"
|
||||
ContentTypeText = "text/plain; charset=utf-8"
|
||||
ContentTypePDF = "application/pdf"
|
||||
ContentTypePNG = "image/png"
|
||||
ContentTypeJPEG = "image/jpeg"
|
||||
ContentTypeSVG = "image/svg+xml"
|
||||
ContentTypeAVC = "video/mp4; codecs=\"avc1\""
|
||||
ContentTypeHEVC = "video/mp4; codecs=\"hvc1.1.6.L93.90\""
|
||||
ContentTypeOGG = "video/ogg"
|
||||
ContentTypeWebM = "video/webm"
|
||||
ContentTypeVP8 = "video/webm; codecs=\"vp8\""
|
||||
ContentTypeVP9 = "video/webm; codecs=\"vp9\""
|
||||
ContentTypeAV1 = "video/webm; codecs=\"av01.0.08M.08\""
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
package video
|
||||
|
||||
type Codec string
|
||||
|
||||
// String returns the codec name as string.
|
||||
func (c Codec) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
// Check browser support: https://cconcolato.github.io/media-mime-support/
|
||||
type Codec = string
|
||||
|
||||
// Video codecs supported by web browsers:
|
||||
// https://cconcolato.github.io/media-mime-support/
|
||||
const (
|
||||
CodecUnknown Codec = ""
|
||||
CodecAVC Codec = "avc1"
|
||||
CodecHVC Codec = "hvc1"
|
||||
CodecHEVC Codec = "hvc1"
|
||||
CodecVVC Codec = "vvc"
|
||||
CodecEVC Codec = "evc"
|
||||
CodecAV1 Codec = "av01"
|
||||
|
|
@ -34,11 +29,14 @@ var Codecs = StandardCodecs{
|
|||
"iso/avc": CodecAVC,
|
||||
"v_mpeg4/avc": CodecAVC,
|
||||
"v_mpeg4/iso/avc": CodecAVC,
|
||||
"hevc": CodecHVC,
|
||||
"hvc": CodecHVC,
|
||||
"hvc1": CodecHVC,
|
||||
"v_hvc": CodecHVC,
|
||||
"v_hvc1": CodecHVC,
|
||||
"hevc": CodecHEVC,
|
||||
"hevC": CodecHEVC,
|
||||
"hvc": CodecHEVC,
|
||||
"hvc1": CodecHEVC,
|
||||
"v_hvc": CodecHEVC,
|
||||
"v_hvc1": CodecHEVC,
|
||||
"hev": CodecHEVC,
|
||||
"hev1": CodecHEVC,
|
||||
"evc": CodecEVC,
|
||||
"evc1": CodecEVC,
|
||||
"evcC": CodecEVC,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ func TestContentType(t *testing.T) {
|
|||
assert.Equal(t, fs.MimeTypeMOV, ContentType(fs.MimeTypeMOV, ""))
|
||||
})
|
||||
t.Run("QuickTime_HVC", func(t *testing.T) {
|
||||
assert.Equal(t, `video/quicktime; codecs="hvc1"`, ContentType(fs.MimeTypeMOV, CodecHVC))
|
||||
assert.Equal(t, `video/quicktime; codecs="hvc1"`, ContentType(fs.MimeTypeMOV, CodecHEVC))
|
||||
})
|
||||
t.Run("MP4", func(t *testing.T) {
|
||||
assert.Equal(t, fs.MimeTypeMP4, ContentType(fs.MimeTypeMP4, ""))
|
||||
|
|
@ -22,6 +22,6 @@ func TestContentType(t *testing.T) {
|
|||
assert.Equal(t, ContentTypeAVC, ContentType(fs.MimeTypeMP4, CodecAVC))
|
||||
})
|
||||
t.Run("MP4_HVC", func(t *testing.T) {
|
||||
assert.Equal(t, `video/mp4; codecs="hvc1"`, ContentType(fs.MimeTypeMP4, CodecHVC))
|
||||
assert.Equal(t, `video/mp4; codecs="hvc1"`, ContentType(fs.MimeTypeMP4, CodecHEVC))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ func Probe(file io.ReadSeeker) (info Info, err error) {
|
|||
// Detect codec by searching for matching chunks.
|
||||
if info.VideoCodec == "" {
|
||||
if found, _ := ChunkHVC1.DataOffset(file); found > 0 {
|
||||
info.VideoCodec = CodecHVC
|
||||
info.VideoCodec = CodecHEVC
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ func TestProbeFile(t *testing.T) {
|
|||
assert.Equal(t, int64(0), info.VideoOffset)
|
||||
assert.Equal(t, int64(-1), info.ThumbOffset)
|
||||
assert.Equal(t, media.Video, info.MediaType)
|
||||
assert.Equal(t, CodecHVC, info.VideoCodec)
|
||||
assert.Equal(t, CodecHEVC, info.VideoCodec)
|
||||
assert.Equal(t, fs.MimeTypeMOV, info.VideoMimeType)
|
||||
assert.Equal(t, ContentTypeMOV+`; codecs="hvc1"`, info.VideoContentType())
|
||||
assert.Equal(t, "1.166666666s", info.Duration.String())
|
||||
|
|
|
|||
|
|
@ -2,30 +2,34 @@ package video
|
|||
|
||||
// Types maps identifiers to standards.
|
||||
var Types = Standards{
|
||||
"": AVC,
|
||||
"mp4": MP4,
|
||||
"mpeg4": MP4,
|
||||
"avc": AVC,
|
||||
"avc1": AVC,
|
||||
"hvc": HEVC,
|
||||
"hvc1": HEVC,
|
||||
"hevc": HEVC,
|
||||
"hevC": HEVC,
|
||||
"evc": EVC,
|
||||
"evc1": EVC,
|
||||
"evcC": EVC,
|
||||
"vvc": VVC,
|
||||
"vvc1": VVC,
|
||||
"vvcC": VVC,
|
||||
"vp8": VP8,
|
||||
"vp80": VP8,
|
||||
"vp9": VP9,
|
||||
"vp90": VP9,
|
||||
"av1": AV1,
|
||||
"av01": AV1,
|
||||
"ogg": OGV,
|
||||
"ogv": OGV,
|
||||
"webm": WebM,
|
||||
"": AVC,
|
||||
"mp4": MP4,
|
||||
"mpeg4": MP4,
|
||||
"avc": AVC,
|
||||
"avc1": AVC,
|
||||
"hevc": HEVC,
|
||||
"hevC": HEVC,
|
||||
"hvc": HEVC,
|
||||
"hvc1": HEVC,
|
||||
"v_hvc": HEVC,
|
||||
"v_hvc1": HEVC,
|
||||
"hev": HEVC,
|
||||
"hev1": HEVC,
|
||||
"evc": EVC,
|
||||
"evc1": EVC,
|
||||
"evcC": EVC,
|
||||
"vvc": VVC,
|
||||
"vvc1": VVC,
|
||||
"vvcC": 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.
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ var AVC = Type{
|
|||
|
||||
// HEVC aka High Efficiency Video Coding (H.265).
|
||||
var HEVC = Type{
|
||||
Codec: CodecHVC,
|
||||
Codec: CodecHEVC,
|
||||
FileType: fs.VideoHEVC,
|
||||
WidthLimit: 0,
|
||||
HeightLimit: 0,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue