Skip to content

Commit

Permalink
Video: Allow re-trimming of already trimmed video (#9315)
Browse files Browse the repository at this point in the history
  • Loading branch information
Morten Barklund authored Oct 10, 2021
1 parent 523018e commit 8884048
Show file tree
Hide file tree
Showing 11 changed files with 228 additions and 60 deletions.
36 changes: 36 additions & 0 deletions packages/media/src/getMsFromHMS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2021 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.
*/

/**
* Converts time in H:M:S format to milliseconds.
*
* @param {string} time Time in HH:MM:SS or H:M:S format.
* @return {number} Milliseconds.
*/
function getMsFromHMS(time) {
if (!time) {
return 0;
}
const parts = time.split(':');
if (parts.length !== 3) {
return 0;
}
const seconds =
parseFloat(parts[2]) + parseInt(parts[1]) * 60 + parseInt(parts[0]) * 3600;
return Math.round(1000 * seconds);
}

export default getMsFromHMS;
1 change: 1 addition & 0 deletions packages/media/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export { default as getFileNameWithExt } from './getFileNameWithExt';
export { default as getExtensionFromMimeType } from './getExtensionFromMimeType';
export { default as getFirstFrameOfVideo } from './getFirstFrameOfVideo';
export { default as getImageDimensions } from './getImageDimensions';
export { default as getMsFromHMS } from './getMsFromHMS';
export { default as getVideoDimensions } from './getVideoDimensions';
export { default as getVideoLength } from './getVideoLength';
export { default as getVideoLengthDisplay } from './getVideoLengthDisplay';
Expand Down
34 changes: 34 additions & 0 deletions packages/media/src/test/getMsFromHMS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2021 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.
*/

/**
* Internal dependencies
*/
import getMsFromHMS from '../getMsFromHMS';

describe('getMsFromHMS', () => {
it('should correctly format values resulting in 0', () => {
expect(getMsFromHMS('00:00:00')).toBe(0);
expect(getMsFromHMS(null)).toBe(0);
expect(getMsFromHMS('foo')).toBe(0);
});

it('should return correct results', () => {
expect(getMsFromHMS('00:00:01')).toBe(1000);
expect(getMsFromHMS('00:01:00')).toBe(60000);
expect(getMsFromHMS('00:00:10.5')).toBe(10500);
});
});
6 changes: 6 additions & 0 deletions packages/story-editor/src/app/api/apiProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function APIProvider({ children }) {
saveStoryById,
autoSaveById,
getMedia,
getMediaById,
uploadMedia,
updateMedia,
deleteMedia,
Expand Down Expand Up @@ -119,6 +120,11 @@ function APIProvider({ children }) {
[media, getMedia]
);

actions.getMediaById = useCallback(
(mediaId) => getMediaById(mediaId, media),
[getMediaById, media]
);

actions.uploadMedia = useCallback(
(file, additionalData) => uploadMedia(file, additionalData, media),
[media, uploadMedia]
Expand Down
31 changes: 15 additions & 16 deletions packages/story-editor/src/app/media/utils/useProcessMedia.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,11 @@ function useProcessMedia({
);

const updateExistingElements = useCallback(
({ oldResource }) => {
const { id } = oldResource;
({ oldResource: resource }) => {
const { id } = resource;
updateElementsByResourceId({
id,
properties: () => {
return {
resource: {
...oldResource,
},
};
},
properties: () => ({ resource }),
});
},
[updateElementsByResourceId]
Expand Down Expand Up @@ -186,19 +180,21 @@ function useProcessMedia({
* @param {string} end Time stamp of end time of new video. Example '00:02:00'.
*/
const trimExistingVideo = useCallback(
({ resource: oldResource, start, end }) => {
const { src: url, mimeType, poster } = oldResource;
({ resource: oldResource, canvasResourceId, start, end }) => {
const { id, src: url, mimeType, poster } = oldResource;

const canvasResource = { ...oldResource, id: canvasResourceId };

const trimData = {
original: oldResource.id,
original: id,
start,
end,
};

const onUploadStart = () => {
updateExistingElements({
oldResource: {
...oldResource,
...canvasResource,
trimData,
isTrimming: true,
},
Expand All @@ -207,7 +203,10 @@ function useProcessMedia({

const onUploadError = () => {
updateExistingElements({
oldResource: { ...oldResource, isTrimming: false },
oldResource: {
...canvasResource,
isTrimming: false,
},
});
};

Expand All @@ -224,9 +223,9 @@ function useProcessMedia({
};

const onUploadProgress = ({ resource }) => {
const oldResourceWithId = { ...resource, id: oldResource.id };
const newResourceWithCanvasId = { ...resource, id: canvasResourceId };
updateExistingElements({
oldResource: oldResourceWithId,
oldResource: newResourceWithCanvasId,
});
};

Expand Down
24 changes: 10 additions & 14 deletions packages/story-editor/src/components/videoTrim/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,17 @@ import { trackEvent } from '@web-stories-wp/tracking';
/**
* Internal dependencies
*/
import { useLocalMedia, useStory } from '../../app';
import { useLocalMedia } from '../../app';
import VideoTrimContext from './videoTrimContext';
import useVideoTrimMode from './useVideoTrimMode';
import useVideoNode from './useVideoNode';

function VideoTrimProvider({ children }) {
const { selectedElements } = useStory(({ state: { selectedElements } }) => ({
selectedElements,
}));
const { trimExistingVideo } = useLocalMedia((state) => ({
trimExistingVideo: state.actions.trimExistingVideo,
}));
const { isTrimMode, hasTrimMode, toggleTrimMode } = useVideoTrimMode();
const { isTrimMode, hasTrimMode, toggleTrimMode, videoData } =
useVideoTrimMode();
const {
hasChanged,
currentTime,
Expand All @@ -49,10 +47,10 @@ function VideoTrimProvider({ children }) {
setVideoNode,
resetOffsets,
setIsDraggingHandles,
} = useVideoNode();
} = useVideoNode(videoData);

const performTrim = useCallback(() => {
const { resource } = selectedElements[0];
const { resource, element } = videoData;
if (!resource) {
return;
}
Expand All @@ -63,6 +61,9 @@ function VideoTrimProvider({ children }) {
length: lengthInSeconds,
lengthFormatted: getVideoLengthDisplay(lengthInSeconds),
},
// This is the ID of the resource, that's currently on canvas and needs to be cloned.
// It's only different from the above resource, if the canvas resource is a trim of the other.
canvasResourceId: element.resource.id,
start: formatMsToHMS(startOffset),
end: formatMsToHMS(endOffset),
});
Expand All @@ -73,16 +74,11 @@ function VideoTrimProvider({ children }) {
end_offset: endOffset,
});
toggleTrimMode();
}, [
endOffset,
startOffset,
trimExistingVideo,
selectedElements,
toggleTrimMode,
]);
}, [endOffset, startOffset, trimExistingVideo, toggleTrimMode, videoData]);

const value = {
state: {
videoData,
hasChanged,
isTrimMode,
hasTrimMode,
Expand Down
25 changes: 15 additions & 10 deletions packages/story-editor/src/components/videoTrim/useVideoNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
*/
import { MEDIA_VIDEO_MINIMUM_DURATION } from '../../constants';

function useVideoNode() {
function useVideoNode(videoData) {
const [currentTime, setCurrentTime] = useState(null);
const [startOffset, rawSetStartOffset] = useState(null);
const [originalStartOffset, setOriginalStartOffset] = useState(null);
Expand Down Expand Up @@ -69,26 +69,31 @@ function useVideoNode() {
}, [paused, isDraggingHandles]);

useEffect(() => {
if (!videoNode) {
if (!videoNode || !videoData) {
return undefined;
}

function restart(at) {
videoNode.currentTime = at / 1000;
videoNode.play();
}

function onLoadedMetadata(evt) {
const duration = Math.floor(evt.target.duration * 1000);
rawSetStartOffset(0);
setOriginalStartOffset(0);
setCurrentTime(0);
rawSetEndOffset(duration);
setOriginalEndOffset(duration);
rawSetStartOffset(videoData.start);
setOriginalStartOffset(videoData.start);
setCurrentTime(videoData.start);
rawSetEndOffset(videoData.end ?? duration);
setOriginalEndOffset(videoData.end ?? duration);
setMaxOffset(duration);
restart(videoData.start);
}
function onTimeUpdate(evt) {
const currentOffset = Math.floor(evt.target.currentTime * 1000);
setCurrentTime(Math.min(currentOffset, endOffset));
// If we've reached the end of the video, start again unless the user has paused the video.
if (currentOffset > endOffset && !isPausedTracker.current) {
videoNode.currentTime = startOffset / 1000;
videoNode.play();
restart(startOffset);
}
}
videoNode.addEventListener('timeupdate', onTimeUpdate);
Expand All @@ -98,7 +103,7 @@ function useVideoNode() {
videoNode.removeEventListener('timeupdate', onTimeUpdate);
videoNode.removeEventListener('loadedmetadata', onLoadedMetadata);
};
}, [startOffset, endOffset, videoNode]);
}, [startOffset, endOffset, videoData, videoNode]);

const setStartOffset = useCallback(
(offset) => {
Expand Down
50 changes: 47 additions & 3 deletions packages/story-editor/src/components/videoTrim/useVideoTrimMode.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@
* External dependencies
*/
import { useFeature } from 'flagged';
import { useCallback, useMemo } from '@web-stories-wp/react';
import { useCallback, useMemo, useState } from '@web-stories-wp/react';
import { trackEvent } from '@web-stories-wp/tracking';
import { getMsFromHMS } from '@web-stories-wp/media';

/**
* Internal dependencies
*/
import { useCanvas, useStory } from '../../app';
import { useCanvas, useStory, useAPI } from '../../app';
import useFFmpeg from '../../app/media/utils/useFFmpeg';

function useVideoTrimMode() {
Expand All @@ -44,6 +45,10 @@ function useVideoTrimMode() {
const { selectedElement } = useStory(({ state: { selectedElements } }) => ({
selectedElement: selectedElements.length === 1 ? selectedElements[0] : null,
}));
const {
actions: { getMediaById },
} = useAPI();
const [videoData, setVideoData] = useState(null);

const toggleTrimMode = useCallback(() => {
if (isEditing) {
Expand All @@ -54,11 +59,49 @@ function useVideoTrimMode() {
hasEditMenu: true,
showOverflow: false,
});

const { resource } = selectedElement;
const { trimData } = resource;

const defaultVideoData = {
element: selectedElement,
resource,
start: 0,
end: null,
};

if (trimData?.original) {
// First clear any existing data
setVideoData(null);
// Load correct video resource
getMediaById(trimData.original)
.then(
// If exists, use as resource with offsets
(originalResource) => ({
element: selectedElement,
resource: originalResource,
start: getMsFromHMS(trimData.start),
end: getMsFromHMS(trimData.end),
}),
// If load fails, pretend there's no original
() => defaultVideoData
)
// Regardless, set resulting data as video data
.then((data) => setVideoData(data));
} else {
setVideoData(defaultVideoData);
}
}
trackEvent('video_trim_mode_toggled', {
status: isEditing ? 'closed' : 'open',
});
}, [isEditing, clearEditing, setEditingElementWithState, selectedElement]);
}, [
isEditing,
clearEditing,
setEditingElementWithState,
selectedElement,
getMediaById,
]);

const { isTranscodingEnabled } = useFFmpeg();

Expand All @@ -74,6 +117,7 @@ function useVideoTrimMode() {
isTrimMode: isEditing && isTrimMode,
hasTrimMode,
toggleTrimMode,
videoData,
};
}

Expand Down
Loading

0 comments on commit 8884048

Please sign in to comment.