Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Media: Skip video optimization based on criteria #11454

Merged
merged 36 commits into from
Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5c62058
Allow skipping video optimization based on criteria
swissspidy May 8, 2022
d3f160a
Update tests
swissspidy May 9, 2022
a00e4dd
Use optional chaining for trim
swissspidy May 9, 2022
cf508e4
Merge branch 'main' into try/11405-mediainfo
swissspidy Jun 15, 2022
29872ea
Merge branch 'main' into try/11405-mediainfo
swissspidy Jun 17, 2022
d3ba75f
Merge branch 'main' into try/11405-mediainfo
swissspidy Jun 24, 2022
1dfcc24
Add tests for `isConsideredOptimized`
swissspidy Jun 24, 2022
b66ccd1
More getFileInfo tests
swissspidy Jun 24, 2022
e217998
Track event
swissspidy Jun 26, 2022
5db096c
Do not override pre-existing mediaSource if provided
swissspidy Jun 26, 2022
6fb1f07
Merge branch 'main' into try/11405-mediainfo
swissspidy Jun 26, 2022
6ade9d4
Mock getTimeTracker
swissspidy Jun 26, 2022
f938e72
Merge branch 'main' into try/11405-mediainfo
swissspidy Jun 27, 2022
0ac1532
Merge branch 'main' into try/11405-mediainfo
swissspidy Jul 1, 2022
cbd8e59
Add todo
swissspidy Jul 1, 2022
1030d79
Merge branch 'main' into try/11405-mediainfo
swissspidy Jul 4, 2022
8b65a5c
Refactor to allow for short circuiting
swissspidy Jul 4, 2022
438b837
Use const
swissspidy Jul 4, 2022
e306432
Take duration into account, add explanation
swissspidy Jul 4, 2022
344e2aa
Merge branch 'main' into try/11405-mediainfo
swissspidy Jul 4, 2022
005d50a
Merge branch 'main' into try/11405-mediainfo
spacedmonkey Jul 6, 2022
332592b
Merge branch 'main' into try/11405-mediainfo
swissspidy Jul 7, 2022
f6fb6c7
Merge branch 'main' into try/11405-mediainfo
swissspidy Jul 8, 2022
212fa86
Fix image stretching in library
swissspidy Jul 12, 2022
b48aa48
New pending/preparing states
swissspidy Jul 12, 2022
8892a7d
Merge branch 'main' into try/11405-mediainfo
swissspidy Jul 12, 2022
4c968ca
Slight rewrite in `useMediaInfo`
swissspidy Jul 12, 2022
173b808
Fix dimension check
swissspidy Jul 12, 2022
f9d6ad2
Reject small frame rate
swissspidy Jul 13, 2022
e2b55ae
Merge branch 'main' into try/11405-mediainfo
swissspidy Jul 13, 2022
dba641b
Lint fix
swissspidy Jul 13, 2022
2412dc9
Fix dimensions check
swissspidy Jul 13, 2022
4fa932d
Update comment
swissspidy Jul 13, 2022
f4f9b48
Add new states to selectors
swissspidy Jul 13, 2022
c637c72
Fix incorrect comparison operators
swissspidy Jul 18, 2022
627a736
Merge branch 'main' into try/11405-mediainfo
swissspidy Jul 18, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions includes/Admin/Editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,7 @@ public function get_editor_settings(): array {
'encodeMarkup' => $this->decoder->supports_decoding(),
'metaBoxes' => $this->meta_boxes->get_meta_boxes_per_location(),
'ffmpegCoreUrl' => trailingslashit( WEBSTORIES_CDN_URL ) . 'js/@ffmpeg/core@0.10.0/dist/ffmpeg-core.js',
'mediainfoUrl' => trailingslashit( WEBSTORIES_CDN_URL ) . 'js/mediainfo.js@0.1.7/dist/mediainfo.min.js',
'flags' => array_merge(
$this->experiments->get_experiment_statuses( 'general' ),
$this->experiments->get_experiment_statuses( 'editor' )
Expand Down
96 changes: 96 additions & 0 deletions packages/story-editor/src/app/media/utils/test/useMediaInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright 2022 Google LLC
*
* 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
*
* https://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.
*/

/**
* External dependencies
*/
import { renderHook } from '@testing-library/react-hooks';

/**
* Internal dependencies
*/
import { ConfigProvider } from '../../../config';
import useMediaInfo from '../useMediaInfo';

function arrange() {
const configState = {
mediainfoUrl: 'https://example.com',
};

return renderHook(() => useMediaInfo(), {
wrapper: ({ children }) => (
<ConfigProvider config={configState}>{children}</ConfigProvider>
),
});
}

const analyzeData = jest.fn();
const mediaInfo = jest.fn(() =>
Promise.resolve({
analyzeData,
close: jest.fn(),
})
);

describe('useMediaInfo', () => {
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
let mediaInfoScript;

beforeAll(() => {
// Tricks loadScriptOnce() into resolving immediately.
mediaInfoScript = document.createElement('script');
mediaInfoScript.src = 'https://example.com';
document.documentElement.appendChild(mediaInfoScript);

window.MediaInfo = mediaInfo;

analyzeData.mockImplementation(() =>
Promise.resolve(
JSON.stringify({
media: {
track: [],
},
})
)
);
});

afterAll(() => {
document.documentElement.removeChild(mediaInfoScript);

delete window.MediaInfo;
});

afterEach(() => {
mediaInfo.mockClear();
});

describe('getFileInfo', () => {
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
it('should return file info', async () => {
const { result } = arrange();

const fileInfo = await result.current.getFileInfo(
new File(['foo'], 'foo.mov', {
type: 'video/quicktime',
})
);

expect(fileInfo).toMatchObject({
isMuted: true,
mimeType: 'video/quicktime',
});
});
});
});
181 changes: 181 additions & 0 deletions packages/story-editor/src/app/media/utils/useMediaInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright 2022 Google LLC
*
* 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
*
* https://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.
*/
/**
* External dependencies
*/
import { useCallback, useMemo } from '@googleforcreators/react';
import { getTimeTracker, trackError } from '@googleforcreators/tracking';

/**
* Internal dependencies
*/
import { useConfig } from '../../config';
import { MEDIA_VIDEO_DIMENSIONS_THRESHOLD } from '../../../constants';

function loadScriptOnce(url) {
if (document.querySelector(`script[src="${url}"]`)) {
return Promise.resolve();
}

return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.async = true;
script.crossOrigin = 'anonymous';
script.src = url;
script.addEventListener('load', resolve);
script.addEventListener('error', reject);
document.head.appendChild(script);
});
}

/**
* @typedef MediaInfo
* @property {string} mimeType File mime type.
* @property {number} fileSize File size in bytes.
* @property {string} format File format.
* @property {string} codec File codec.
* @property {number} frameRate Frame rate (rounded).
* @property {number} height Height in px.
* @property {number} width Width in px.
* @property {string} colorSpace Color space.
* @property {number} duration Video duration.
* @property {string} videoCodec Video codec.
* @property {string} audioCodec Audio codec.
* @property {boolean} isMuted Whether the video is muted.
*/

/**
* Custom hook to interact with mediainfo.js.
*
* @see https://mediainfo.js.org/
* @return {Object} Functions and vars related to mediainfo.js usage.
*/
function useMediaInfo() {
const { mediainfoUrl } = useConfig();

const getFileInfo = useCallback(
/**
* Returns information about a given media file.
*
* @param {File} file File object.
* @return {Promise<MediaInfo|null>} File info or null on error.
*/
async (file) => {
const getSize = () => file.size;

const readChunk = (chunkSize, offset) =>
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
if (event.target.error) {
reject(event.target.error);
}
resolve(new Uint8Array(event.target.result));
};
reader.readAsArrayBuffer(file.slice(offset, offset + chunkSize));
});

//eslint-disable-next-line @wordpress/no-unused-vars-before-return -- False positive because of the finally().
const trackTiming = getTimeTracker('load_mediainfo');

try {
// This will expose the window.MediaInfo global.
await loadScriptOnce(mediainfoUrl);

const mediaInfo = await window.MediaInfo({ format: 'JSON' });
const result = JSON.parse(
await mediaInfo.analyzeData(getSize, readChunk)
);

swissspidy marked this conversation as resolved.
Show resolved Hide resolved
const normalizedResult = result.media.track.reduce(
(acc, track) => {
if (track['@type'] === 'General') {
acc.fileSize = Number(track.FileSize);
acc.format = track.Format.toLowerCase().replace('mpeg-4', 'mp4');
acc.frameRate = Number(Number(track.FrameRate).toFixed(0));
acc.codec = track.CodecID.trim();
}

if (track['@type'] === 'Image' || track['@type'] === 'Video') {
acc.width = Number(track.Width);
acc.height = Number(track.Height);
acc.colorSpace = track.ColorSpace; // Maybe useful in the future.
}

if (track['@type'] === 'Video') {
acc.duration = Number(track.Duration);
acc.videoCodec = track.Format.toLowerCase();
}

if (track['@type'] === 'Audio') {
acc.audioCodec = track.Format.toLowerCase();
}

return acc;
},
{
mimeType: file.type,
}
);

normalizedResult.isMuted = !normalizedResult.audioCodec;

mediaInfo.close();

return normalizedResult;
} catch (err) {
// eslint-disable-next-line no-console -- We want to surface this error.
console.error(err);

trackError('mediainfo', err.message);

return null;
} finally {
trackTiming();
}
},
[mediainfoUrl]
);

const isConsideredOptimized = useCallback((fileInfo) => {
if (!fileInfo) {
return false;
}

const hasSmallFileSize = fileInfo.fileSize < 5_000_000;
const hasSmallDimensions =
fileInfo.width * fileInfo.height <=
MEDIA_VIDEO_DIMENSIONS_THRESHOLD.WIDTH *
MEDIA_VIDEO_DIMENSIONS_THRESHOLD.HEIGHT;

// Video is small enough and has an allowed mime type, upload straight away.
return (
hasSmallFileSize &&
hasSmallDimensions &&
['video/webm', 'video/mp4'].includes(fileInfo.mimeType)
);
}, []);

return useMemo(
() => ({
getFileInfo,
isConsideredOptimized,
}),
[getFileInfo, isConsideredOptimized]
);
}

export default useMediaInfo;
Loading