Make video sizing consistent with images (#8102)

* Make video sizing consistent with images

* Test suggestedSize

* Constrain width of media in large mode
This commit is contained in:
Robin 2022-03-22 18:16:03 -04:00 committed by GitHub
parent bff1ef31d6
commit 953e3148d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 90 additions and 89 deletions

View file

@ -16,8 +16,6 @@ limitations under the License.
span.mx_MVideoBody { span.mx_MVideoBody {
video.mx_MVideoBody { video.mx_MVideoBody {
max-width: 100%;
height: auto;
border-radius: $timeline-image-border-radius; border-radius: $timeline-image-border-radius;
} }
} }

View file

@ -377,28 +377,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
infoHeight = this.state.loadedImageDimensions.naturalHeight; infoHeight = this.state.loadedImageDimensions.naturalHeight;
} }
// The maximum size of the thumbnail as it is rendered as an <img> // The maximum size of the thumbnail as it is rendered as an <img>,
// check for any height constraints // accounting for any height constraints
const imageSize = SettingsStore.getValue("Images.size") as ImageSize; const { w: maxWidth, h: maxHeight } = suggestedImageSize(
const isPortrait = infoWidth < infoHeight; SettingsStore.getValue("Images.size") as ImageSize,
const suggestedAndPossibleWidth = Math.min(suggestedImageSize(imageSize, isPortrait).w, infoWidth); { w: infoWidth, h: infoHeight },
const suggestedAndPossibleHeight = Math.min(suggestedImageSize(imageSize, isPortrait).h, infoHeight); forcedHeight ?? this.props.maxImageHeight,
const aspectRatio = infoWidth / infoHeight; );
let maxWidth: number;
let maxHeight: number;
const maxHeightConstraint = forcedHeight || this.props.maxImageHeight || suggestedAndPossibleHeight;
if (maxHeightConstraint * aspectRatio < suggestedAndPossibleWidth || imageSize === ImageSize.Large) {
// The width is dictated by the maximum height that was defined by the props or the function param `forcedHeight`
// If the thumbnail size is set to Large, we always let the size be dictated by the height.
maxWidth = maxHeightConstraint * aspectRatio;
// there is no need to check for infoHeight here since this is done with `maxHeightConstraint * aspectRatio < suggestedAndPossibleWidth`
maxHeight = maxHeightConstraint;
} else {
// height is dictated by suggestedWidth (based on the Image.size setting)
maxWidth = suggestedAndPossibleWidth;
maxHeight = suggestedAndPossibleWidth / aspectRatio;
}
let img = null; let img = null;
let placeholder = null; let placeholder = null;

View file

@ -62,38 +62,6 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
}; };
} }
private suggestedDimensions(isPortrait): { w: number, h: number } {
return suggestedVideoSize(SettingsStore.getValue("Images.size") as ImageSize);
}
private thumbScale(
fullWidth: number,
fullHeight: number,
thumbWidth?: number,
thumbHeight?: number,
): number {
if (!fullWidth || !fullHeight) {
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
// log this because it's spammy
return undefined;
}
if (!thumbWidth || !thumbHeight) {
const dims = this.suggestedDimensions(fullWidth < fullHeight);
thumbWidth = dims.w;
thumbHeight = dims.h;
}
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
// no scaling needs to be applied
return 1;
}
// always scale the videos based on their width.
const widthMulti = thumbWidth / fullWidth;
return widthMulti;
}
private getContentUrl(): string|null { private getContentUrl(): string|null {
const content = this.props.mxEvent.getContent<IMediaEventContent>(); const content = this.props.mxEvent.getContent<IMediaEventContent>();
// During export, the content url will point to the MSC, which will later point to a local url // During export, the content url will point to the MSC, which will later point to a local url
@ -135,13 +103,10 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
let width = info.w; const { w: width, h: height } = suggestedVideoSize(
let height = info.h; SettingsStore.getValue("Images.size") as ImageSize,
const scale = this.thumbScale(info.w, info.h); { w: info.w, h: info.h },
if (scale) { );
width = Math.floor(info.w * scale);
height = Math.floor(info.h * scale);
}
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
@ -286,24 +251,18 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
); );
} }
const { w: maxWidth, h: maxHeight } = suggestedVideoSize(
SettingsStore.getValue("Images.size") as ImageSize,
{ w: content.info?.w, h: content.info?.h },
);
const contentUrl = this.getContentUrl(); const contentUrl = this.getContentUrl();
const thumbUrl = this.getThumbUrl(); const thumbUrl = this.getThumbUrl();
const defaultDims = this.suggestedDimensions(false);
let height = defaultDims.h;
let width = defaultDims.w;
let poster = null; let poster = null;
let preload = "metadata"; let preload = "metadata";
if (content.info) { if (content.info && thumbUrl) {
const scale = this.thumbScale(content.info.w, content.info.h); poster = thumbUrl;
if (scale) { preload = "none";
width = Math.floor(content.info.w * scale);
height = Math.floor(content.info.h * scale);
}
if (thumbUrl) {
poster = thumbUrl;
preload = "none";
}
} }
const fileBody = this.getFileBody(); const fileBody = this.getFileBody();
@ -318,8 +277,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState>
preload={preload} preload={preload}
muted={autoplay} muted={autoplay}
autoPlay={autoplay} autoPlay={autoplay}
height={height} style={{ maxHeight, maxWidth }}
width={width}
poster={poster} poster={poster}
onPlay={this.videoOnPlay} onPlay={this.videoOnPlay}
/> />

View file

@ -15,24 +15,46 @@ limitations under the License.
*/ */
// For Large the image gets drawn as big as possible. // For Large the image gets drawn as big as possible.
// constraint by: timeline width, manual heigh overrides, SIZE_LARGE.h // constraint by: timeline width, manual height overrides, SIZE_LARGE.h
const SIZE_LARGE = { w: 800, h: 600 }; const SIZE_LARGE = { w: 800, h: 600 };
// For Normal the image gets drawn to never exceed SIZE_NORMAL.w, SIZE_NORMAL.h // For Normal the image gets drawn to never exceed SIZE_NORMAL.w, SIZE_NORMAL.h
// constraint by: timeline width, manual heigh overrides // constraint by: timeline width, manual height overrides
const SIZE_NORMAL_LANDSCAPE = { w: 324, h: 324 }; // for w > h const SIZE_NORMAL_LANDSCAPE = { w: 324, h: 324 }; // for w > h
const SIZE_NORMAL_PORTRAIT = { w: 324 * (9/16), h: 324 }; // for h > w const SIZE_NORMAL_PORTRAIT = { w: 324 * (9/16), h: 324 }; // for h > w
type Dimensions = { w: number, h: number };
export enum ImageSize { export enum ImageSize {
Normal = "normal", Normal = "normal",
Large = "large", Large = "large",
} }
export function suggestedSize(size: ImageSize, portrait = false): { w: number, h: number} { /**
switch (size) { * @param {ImageSize} size The user's image size preference
case ImageSize.Large: * @param {Dimensions} contentSize The natural dimensions of the content
return SIZE_LARGE; * @param {number} maxHeight Overrides the default height limit
case ImageSize.Normal: * @returns {Dimensions} The suggested maximum dimensions for the image
default: */
return portrait ? SIZE_NORMAL_PORTRAIT : SIZE_NORMAL_LANDSCAPE; export function suggestedSize(size: ImageSize, contentSize: Dimensions, maxHeight?: number): Dimensions {
const aspectRatio = contentSize.w / contentSize.h;
const portrait = aspectRatio < 1;
const maxSize = (size === ImageSize.Large) ? SIZE_LARGE :
portrait ? SIZE_NORMAL_PORTRAIT : SIZE_NORMAL_LANDSCAPE;
if (!contentSize.w || !contentSize.h) {
return maxSize;
}
const constrainedSize = {
w: Math.min(maxSize.w, contentSize.w),
h: maxHeight ? Math.min(maxSize.h, contentSize.h, maxHeight) : Math.min(maxSize.h, contentSize.h),
};
if (constrainedSize.h * aspectRatio < constrainedSize.w) {
// Height dictates width
return { w: constrainedSize.h * aspectRatio, h: constrainedSize.h };
} else {
// Width dictates height
return { w: constrainedSize.w, h: constrainedSize.w / aspectRatio };
} }
} }

View file

@ -0,0 +1,38 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ImageSize, suggestedSize } from "../../../src/settings/enums/ImageSize";
describe("ImageSize", () => {
describe("suggestedSize", () => {
it("constrains width", () => {
const size = suggestedSize(ImageSize.Normal, { w: 648, h: 162 });
expect(size).toStrictEqual({ w: 324, h: 81 });
});
it("constrains height", () => {
const size = suggestedSize(ImageSize.Normal, { w: 162, h: 648 });
expect(size).toStrictEqual({ w: 81, h: 324 });
});
it("constrains width in large mode", () => {
const size = suggestedSize(ImageSize.Large, { w: 2400, h: 1200 });
expect(size).toStrictEqual({ w: 800, h: 400 });
});
it("returns max values if content size is not specified", () => {
const size = suggestedSize(ImageSize.Normal, { w: null, h: null });
expect(size).toStrictEqual({ w: 324, h: 324 });
});
});
});