diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 178cf5324f225..d95ce8fc082c0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2876,8 +2876,8 @@ importers:
specifier: ^5.3.4
version: 5.3.4(react@18.3.1)
tus-js-client:
- specifier: 2.3.0
- version: 2.3.0
+ specifier: 4.1.0
+ version: 4.1.0
devDependencies:
'@automattic/calypso-color-schemes':
specifier: 3.1.3
@@ -11202,6 +11202,9 @@ packages:
js-base64@2.6.4:
resolution: {integrity: sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==}
+ js-base64@3.7.7:
+ resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -12650,6 +12653,11 @@ packages:
engines: {node: '>=14'}
hasBin: true
+ prettier@3.3.3:
+ resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==}
+ engines: {node: '>=14'}
+ hasBin: true
+
pretty-error@4.0.0:
resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==}
@@ -12698,6 +12706,9 @@ packages:
resolution: {integrity: sha512-rjaeGbsmhNDcDInmwi4MuI6mRwJu6zq8GjYCLuSuE7GF+4UjgzkL69sVKKJ2T2xH61kK7rXvGYpvaTu909oXaQ==}
engines: {node: '>=4.0.0'}
+ proper-lockfile@4.1.2:
+ resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==}
+
properties@1.2.1:
resolution: {integrity: sha512-qYNxyMj1JeW54i/EWEFsM1cVwxJbtgPp8+0Wg9XjNaK6VE/c4oRi6PNu5p7w1mNXEIQIjV5Wwn8v8Gz82/QzdQ==}
engines: {node: '>=0.10'}
@@ -13213,6 +13224,10 @@ packages:
retry@0.10.1:
resolution: {integrity: sha512-ZXUSQYTHdl3uS7IuCehYfMzKyIDBNoAuUblvy5oGO5UJSUTmStUUVPXbA9Qxd173Bgre53yCQczQuHgRWAdvJQ==}
+ retry@0.12.0:
+ resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
+ engines: {node: '>= 4'}
+
retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
@@ -14070,6 +14085,10 @@ packages:
tus-js-client@2.3.0:
resolution: {integrity: sha512-I4cSwm6N5qxqCmBqenvutwSHe9ntf81lLrtf6BmLpG2v4wTl89atCQKqGgqvkodE6Lx+iKIjMbaXmfvStTg01g==}
+ tus-js-client@4.1.0:
+ resolution: {integrity: sha512-e/nC/kJahvNYBcnwcqzuhFIvVELMMpbVXIoOOKdUn74SdQCvJd2JjqV2jZLv2EFOVbV4qLiO0lV7BxBXF21b6Q==}
+ engines: {node: '>=18'}
+
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@@ -17680,7 +17699,7 @@ snapshots:
node-fetch: 2.6.7
picomatch: 2.3.1
pkg-dir: 5.0.0
- prettier-fallback: prettier@3.3.2
+ prettier-fallback: prettier@3.3.3
pretty-hrtime: 1.0.3
resolve-from: 5.0.0
semver: 7.5.2
@@ -17715,7 +17734,7 @@ snapshots:
node-fetch: 2.6.7
picomatch: 2.3.1
pkg-dir: 5.0.0
- prettier-fallback: prettier@3.3.2
+ prettier-fallback: prettier@3.3.3
pretty-hrtime: 1.0.3
resolve-from: 5.0.0
semver: 7.5.2
@@ -17750,7 +17769,7 @@ snapshots:
node-fetch: 2.6.7
picomatch: 2.3.1
pkg-dir: 5.0.0
- prettier-fallback: prettier@3.3.2
+ prettier-fallback: prettier@3.3.3
pretty-hrtime: 1.0.3
resolve-from: 5.0.0
semver: 7.5.2
@@ -17787,7 +17806,7 @@ snapshots:
node-fetch: 2.6.7
picomatch: 2.3.1
pkg-dir: 5.0.0
- prettier-fallback: prettier@3.3.2
+ prettier-fallback: prettier@3.3.3
pretty-hrtime: 1.0.3
resolve-from: 5.0.0
semver: 7.5.2
@@ -24241,6 +24260,8 @@ snapshots:
js-base64@2.6.4: {}
+ js-base64@3.7.7: {}
+
js-tokens@4.0.0: {}
js-yaml@3.14.1:
@@ -25857,6 +25878,8 @@ snapshots:
prettier@3.3.2: {}
+ prettier@3.3.3: {}
+
pretty-error@4.0.0:
dependencies:
lodash: 4.17.21
@@ -25910,6 +25933,12 @@ snapshots:
graceful-fs: 4.2.11
retry: 0.10.1
+ proper-lockfile@4.1.2:
+ dependencies:
+ graceful-fs: 4.2.11
+ retry: 0.12.0
+ signal-exit: 3.0.7
+
properties@1.2.1: {}
protocol-buffers-schema@3.6.0: {}
@@ -26536,6 +26565,8 @@ snapshots:
retry@0.10.1: {}
+ retry@0.12.0: {}
+
retry@0.13.1: {}
reusify@1.0.4: {}
@@ -27471,6 +27502,16 @@ snapshots:
proper-lockfile: 2.0.1
url-parse: 1.5.10
+ tus-js-client@4.1.0:
+ dependencies:
+ buffer-from: 1.1.2
+ combine-errors: 3.0.3
+ is-stream: 2.0.1
+ js-base64: 3.7.7
+ lodash.throttle: 4.1.1
+ proper-lockfile: 4.1.2
+ url-parse: 1.5.10
+
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
diff --git a/projects/packages/videopress/package.json b/projects/packages/videopress/package.json
index 465ba1396ff97..0c118f403d9b3 100644
--- a/projects/packages/videopress/package.json
+++ b/projects/packages/videopress/package.json
@@ -84,6 +84,6 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"react-router-dom": "^5.3.4",
- "tus-js-client": "2.3.0"
+ "tus-js-client": "4.1.0"
}
}
diff --git a/projects/packages/videopress/src/class-xmlrpc.php b/projects/packages/videopress/src/class-xmlrpc.php
index 36932c8c6590b..2bfb544fb47e9 100644
--- a/projects/packages/videopress/src/class-xmlrpc.php
+++ b/projects/packages/videopress/src/class-xmlrpc.php
@@ -85,7 +85,7 @@ public function xmlrpc_methods( $methods, $core_methods, $user ) { // phpcs:igno
* @return array
*/
public function create_media_item( $media ) {
- $this->authenticate_user();
+ wplog( $this->authenticate_user() );
foreach ( $media as & $media_item ) {
$title = sanitize_title( basename( $media_item['url'] ) );
diff --git a/projects/packages/videopress/src/client/admin/components/admin-page/index.tsx b/projects/packages/videopress/src/client/admin/components/admin-page/index.tsx
index 914c33ed28a97..7178449b89919 100644
--- a/projects/packages/videopress/src/client/admin/components/admin-page/index.tsx
+++ b/projects/packages/videopress/src/client/admin/components/admin-page/index.tsx
@@ -45,8 +45,17 @@ import styles from './styles.module.scss';
const useDashboardVideos = () => {
const { uploadVideo, uploadVideoFromLibrary, setVideosQuery } = useDispatch( STORE_ID );
- const { items, uploading, uploadedVideoCount, isFetching, search, page, itemsPerPage, total } =
- useVideos();
+ const {
+ items,
+ uploadErrors,
+ uploading,
+ uploadedVideoCount,
+ isFetching,
+ search,
+ page,
+ itemsPerPage,
+ total,
+ } = useVideos();
const { items: localVideos, uploadedLocalVideoCount } = useLocalVideos();
const { hasVideoPressPurchase } = usePlan();
@@ -105,9 +114,10 @@ const useDashboardVideos = () => {
}, [ totalOfPages, page, pageFromSearchParam, search, searchFromSearchParam, tempPage.current ] );
// Do not show uploading videos if not in the first page or searching
- let videos = page > 1 || Boolean( search ) ? items : [ ...uploading, ...items ];
+ let videos = page > 1 || Boolean( search ) ? items : [ ...uploadErrors, ...uploading, ...items ];
- const hasVideos = uploadedVideoCount > 0 || isFetching || uploading?.length > 0;
+ const hasVideos =
+ uploadedVideoCount > 0 || isFetching || uploading?.length > 0 || uploadErrors?.length > 0;
const hasLocalVideos = uploadedLocalVideoCount > 0;
const handleFilesUpload = ( files: File[] ) => {
@@ -144,7 +154,7 @@ const useDashboardVideos = () => {
handleFilesUpload,
handleLocalVideoUpload,
loading: isFetching,
- uploading: uploading?.length > 0,
+ uploading: uploading?.length > 0 || uploadErrors?.length > 0,
hasVideoPressPurchase,
};
};
diff --git a/projects/packages/videopress/src/client/admin/components/admin-page/libraries.tsx b/projects/packages/videopress/src/client/admin/components/admin-page/libraries.tsx
index 1acfb8fd020a9..adfb95ad11e0a 100644
--- a/projects/packages/videopress/src/client/admin/components/admin-page/libraries.tsx
+++ b/projects/packages/videopress/src/client/admin/components/admin-page/libraries.tsx
@@ -125,10 +125,12 @@ export const VideoPressLibrary = ( { videos, totalVideos, loading }: VideoLibrar
const history = useHistory();
const { search } = useVideos();
const videosToDisplay = [
- // First comes the videos that are uploading
+ // First comes video upload errors
+ ...videos.filter( video => video.error ),
+ // Then comes the videos that are uploading
...videos.filter( video => video.uploading ),
// Then the videos that are not uploading, at most 6
- ...videos.filter( video => ! video.uploading ).slice( 0, 6 ),
+ ...videos.filter( video => ! video.uploading && ! video.error ).slice( 0, 6 ),
];
const libraryTypeFromLocalStorage = localStorage.getItem(
diff --git a/projects/packages/videopress/src/client/admin/components/video-card/error.tsx b/projects/packages/videopress/src/client/admin/components/video-card/error.tsx
new file mode 100644
index 0000000000000..71c82be417b17
--- /dev/null
+++ b/projects/packages/videopress/src/client/admin/components/video-card/error.tsx
@@ -0,0 +1,161 @@
+/**
+ * External dependencies
+ */
+import { Button, Title, useBreakpointMatch } from '@automattic/jetpack-components';
+import { __ } from '@wordpress/i18n';
+import { Icon, chevronDown, chevronUp } from '@wordpress/icons';
+import clsx from 'clsx';
+import { useState } from 'react';
+import { usePermission } from '../../hooks/use-permission';
+import useVideo from '../../hooks/use-video';
+/**
+ * Internal dependencies
+ */
+import PublishFirstVideoPopover from '../publish-first-video-popover';
+import { ConnectVideoQuickActions } from '../video-quick-actions';
+import VideoThumbnail from '../video-thumbnail';
+import styles from './style.module.scss';
+import { VideoCardProps } from './types';
+/**
+ * Types
+ */
+import type React from 'react';
+
+const QuickActions = ( {
+ id,
+ onVideoDetailsClick,
+ className,
+}: {
+ id: VideoCardProps[ 'id' ];
+ onVideoDetailsClick: VideoCardProps[ 'onVideoDetailsClick' ];
+ className?: VideoCardProps[ 'className' ];
+} ) => {
+ const { canPerformAction } = usePermission();
+
+ return (
+
+
+
+ { id && }
+
+ );
+};
+
+/**
+ * Video Card component
+ *
+ * @param {VideoCardProps} props - Component props.
+ * @returns {React.ReactNode} - VideoCardError react component.
+ */
+export const VideoCardError = ( {
+ title,
+ id,
+ duration,
+ plays,
+ thumbnail,
+ editable,
+ showQuickActions = true,
+ loading = false,
+ isUpdatingPoster = false,
+ uploading = false,
+ processing = false,
+ uploadProgress,
+ onVideoDetailsClick,
+}: VideoCardProps ) => {
+ const isBlank = ! title && ! duration && ! plays && ! thumbnail && ! loading;
+
+ const [ anchor, setAnchor ] = useState( null );
+ const [ isSm ] = useBreakpointMatch( 'sm' );
+ const [ isOpen, setIsOpen ] = useState( false );
+ const disabled = false;
+
+ return (
+ <>
+ setIsOpen( wasOpen => ! wasOpen ) } ) }
+ >
+ { ! isSm &&
}
+
+
+
+
+ { isSm && (
+
+ { isOpen && }
+ { ! isOpen && }
+
+ ) }
+
+
+ { title }
+
+
+
+
+
+ { showQuickActions && ! isSm && (
+
+ ) }
+
+
+ { showQuickActions && isSm && isOpen && (
+
+ ) }
+ >
+ );
+};
+
+export const ConnectVideoCard = ( { id, ...restProps }: VideoCardProps ) => {
+ const { isDeleting, uploading, processing, isUpdatingPoster, data, uploadProgress } =
+ useVideo( id );
+
+ const loading = ( isDeleting || restProps?.loading ) && ! uploading && ! processing;
+ const editable = restProps?.editable && ! isDeleting && ! uploading && ! processing;
+
+ return (
+
+ );
+};
+
+export default ConnectVideoCard;
diff --git a/projects/packages/videopress/src/client/admin/components/video-grid/index.tsx b/projects/packages/videopress/src/client/admin/components/video-grid/index.tsx
index 042d8545e702c..4a8be1755641c 100644
--- a/projects/packages/videopress/src/client/admin/components/video-grid/index.tsx
+++ b/projects/packages/videopress/src/client/admin/components/video-grid/index.tsx
@@ -6,6 +6,7 @@ import { Container, Col } from '@automattic/jetpack-components';
* Internal dependencies
*/
import VideoCard from '../video-card';
+import VideoCardError from '../video-card/error';
import styles from './style.module.scss';
import { VideoGridProps } from './types';
import type React from 'react';
@@ -29,15 +30,29 @@ const VideoGrid = ( { videos, count = 6, onVideoDetailsClick, loading }: VideoGr
{ gridVideos.map( ( video, index ) => {
return (
-
+ { video.error ? (
+
+ alert(
+ `${ video.error.error || 'Error' }: ${
+ video.error.message || 'Unknown error'
+ }`
+ )
+ }
+ />
+ ) : (
+
+ ) }
);
} ) }
diff --git a/projects/packages/videopress/src/client/admin/components/video-thumbnail/index.tsx b/projects/packages/videopress/src/client/admin/components/video-thumbnail/index.tsx
index a5f6b171aaf3e..33f3b77963508 100644
--- a/projects/packages/videopress/src/client/admin/components/video-thumbnail/index.tsx
+++ b/projects/packages/videopress/src/client/admin/components/video-thumbnail/index.tsx
@@ -12,7 +12,7 @@ import {
import { Dropdown } from '@wordpress/components';
import { gmdateI18n } from '@wordpress/date';
import { __, sprintf } from '@wordpress/i18n';
-import { Icon, edit, cloud, image, media, video } from '@wordpress/icons';
+import { Icon, edit, cloud, image, media, video, warning } from '@wordpress/icons';
import clsx from 'clsx';
import { forwardRef } from 'react';
/**
@@ -153,6 +153,12 @@ const ProcessingThumbnail = ( { isRow = false }: { isRow?: boolean } ) => (
);
+const ErrorThumbnail = () => (
+
+
+
+);
+
/**
* React component to display video thumbnail.
*
@@ -177,6 +183,7 @@ const VideoThumbnail = forwardRef< HTMLDivElement, VideoThumbnailProps >(
onUploadImage,
uploadProgress,
isRow = false,
+ hasError = false,
},
ref
) => {
@@ -185,6 +192,7 @@ const VideoThumbnail = forwardRef< HTMLDivElement, VideoThumbnailProps >(
// Mapping thumbnail (Ordered by priority)
let thumbnail = defaultThumbnail;
+
thumbnail = loading ? : thumbnail;
thumbnail = uploading ? (
@@ -193,6 +201,8 @@ const VideoThumbnail = forwardRef< HTMLDivElement, VideoThumbnailProps >(
);
thumbnail = processing ? : thumbnail;
+ thumbnail = hasError ? : thumbnail;
+
thumbnail =
typeof thumbnail === 'string' && thumbnail !== '' ? (
diff --git a/projects/packages/videopress/src/client/admin/components/video-thumbnail/style.module.scss b/projects/packages/videopress/src/client/admin/components/video-thumbnail/style.module.scss
index a6973c23d8abb..459ec96043ede 100644
--- a/projects/packages/videopress/src/client/admin/components/video-thumbnail/style.module.scss
+++ b/projects/packages/videopress/src/client/admin/components/video-thumbnail/style.module.scss
@@ -43,6 +43,10 @@
align-items: center;
justify-content: center;
fill: var( --jp-gray-50 );
+
+ &.thumbnail-error {
+ fill: var( --jp-yellow-20 );
+ }
}
}
diff --git a/projects/packages/videopress/src/client/admin/components/video-thumbnail/types.ts b/projects/packages/videopress/src/client/admin/components/video-thumbnail/types.ts
index 016d1add98715..dcc92168971c0 100644
--- a/projects/packages/videopress/src/client/admin/components/video-thumbnail/types.ts
+++ b/projects/packages/videopress/src/client/admin/components/video-thumbnail/types.ts
@@ -82,4 +82,9 @@ export type VideoThumbnailProps = VideoThumbnailDropdownProps & {
* True if the thumbnail is used on a video row.
*/
isRow?: boolean;
+
+ /**
+ * True if the video has an error.
+ */
+ hasError?: boolean;
};
diff --git a/projects/packages/videopress/src/client/admin/hooks/use-videos/index.js b/projects/packages/videopress/src/client/admin/hooks/use-videos/index.js
index 9c352b9f16c10..7f9e876bff5b4 100644
--- a/projects/packages/videopress/src/client/admin/hooks/use-videos/index.js
+++ b/projects/packages/videopress/src/client/admin/hooks/use-videos/index.js
@@ -32,6 +32,7 @@ export default function useVideos() {
const pagination = useSelect( select => select( STORE_ID ).getPagination() );
const storageUsed = useSelect( select => select( STORE_ID ).getStorageUsed(), [] );
const filter = useSelect( select => select( STORE_ID ).getVideosFilter() );
+ const uploadErrors = useSelect( select => select( STORE_ID ).getUploadErrorVideos() );
return {
items,
@@ -48,6 +49,7 @@ export default function useVideos() {
...query,
...pagination,
...storageUsed,
+ uploadErrors,
// Handlers
setPage: page => dispatch( STORE_ID ).setVideosQuery( { page } ),
diff --git a/projects/packages/videopress/src/client/admin/types/index.ts b/projects/packages/videopress/src/client/admin/types/index.ts
index b93b716dfaa01..9547c179db7a4 100644
--- a/projects/packages/videopress/src/client/admin/types/index.ts
+++ b/projects/packages/videopress/src/client/admin/types/index.ts
@@ -143,6 +143,11 @@ export type OriginalVideoPressVideo = {
jetpack_videopress_guid: string;
};
+export type VideoPressVideoError = {
+ error?: string;
+ message?: string;
+};
+
export type VideoPressVideo = {
width?: OriginalVideoPressVideo[ 'media_details' ][ 'width' ];
height?: OriginalVideoPressVideo[ 'media_details' ][ 'height' ];
@@ -169,6 +174,7 @@ export type VideoPressVideo = {
thumbnail?: string;
uploading?: boolean;
plays?: number; // Not provided yet
+ error?: VideoPressVideoError;
};
export type LocalVideo = {
diff --git a/projects/packages/videopress/src/client/lib/resumable-file-uploader/index.ts b/projects/packages/videopress/src/client/lib/resumable-file-uploader/index.ts
index 3a670cacdbcae..c9c7107692b68 100644
--- a/projects/packages/videopress/src/client/lib/resumable-file-uploader/index.ts
+++ b/projects/packages/videopress/src/client/lib/resumable-file-uploader/index.ts
@@ -23,6 +23,13 @@ type UploadVideoArguments = {
tokenData: MediaTokenProps;
};
+const getJwtKey = ( url: string ) => {
+ const parsedUrl = new URL( url );
+ const path = parsedUrl.pathname;
+ const parts = path.split( '/' );
+ return parts.pop();
+};
+
const resumableFileUploader = ( {
file,
tokenData,
@@ -44,7 +51,19 @@ const resumableFileUploader = ( {
filetype: file.type,
},
retryDelays: [ 0, 1000, 3000, 5000, 10000 ],
- onBeforeRequest: function ( req ) {
+ onShouldRetry: function ( err ) {
+ const status = err.originalResponse ? err.originalResponse.getStatus() : 0;
+ // Do not retry if the status is a 400.
+ if ( status === 400 ) {
+ debug( '400 error, cleanup', upload._urlStorageKey );
+ localStorage.removeItem( upload._urlStorageKey );
+ return false;
+ }
+
+ // For any other status code, we retry.
+ return true;
+ },
+ onBeforeRequest: async function ( req ) {
// make ALL requests be either POST or GET to honor the public-api.wordpress.com "contract".
const method = req._method;
@@ -75,10 +94,7 @@ const resumableFileUploader = ( {
}
if ( [ 'OPTIONS', 'GET', 'HEAD', 'DELETE', 'PUT', 'PATCH' ].indexOf( method ) >= 0 ) {
- const url = new URL( req._url );
- const path = url.pathname;
- const parts = path.split( '/' );
- const maybeUploadkey = parts[ parts.length - 1 ];
+ const maybeUploadkey = getJwtKey( req._url );
if ( jwtsForKeys[ maybeUploadkey ] ) {
req.setHeader( 'x-videopress-upload-token', jwtsForKeys[ maybeUploadkey ] );
} else if ( 'HEAD' === method ) {
diff --git a/projects/packages/videopress/src/client/state/actions.js b/projects/packages/videopress/src/client/state/actions.js
index 650c5fb8bde1a..6f6749a7906cc 100644
--- a/projects/packages/videopress/src/client/state/actions.js
+++ b/projects/packages/videopress/src/client/state/actions.js
@@ -3,6 +3,7 @@
*/
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
+import debugFactory from 'debug';
/**
* Internal dependencies
*/
@@ -33,6 +34,7 @@ import {
VIDEO_PRIVACY_LEVELS,
WP_REST_API_MEDIA_ENDPOINT,
SET_VIDEO_UPLOADING,
+ SET_VIDEO_UPLOADING_ERROR,
SET_VIDEO_PROCESSING,
SET_VIDEO_UPLOADED,
SET_IS_FETCHING_PURCHASES,
@@ -57,22 +59,28 @@ import {
import { mapVideoFromWPV2MediaEndpoint } from './utils/map-videos';
import { videoIsPrivate } from './utils/video-is-private';
+const debug = debugFactory( 'videopress:actions' );
+
/**
* Utility function to pool the video data until poster is ready.
*/
const pollingUploadedVideoData = async data => {
+ debug( 'Polling video data', data );
const response = await apiFetch( {
path: addQueryArgs( `${ WP_REST_API_MEDIA_ENDPOINT }/${ data?.id }` ),
} );
+ debug( 'Polling response', response );
const video = mapVideoFromWPV2MediaEndpoint( response );
if ( video?.posterImage !== null && video?.posterImage !== '' ) {
+ debug( 'Video data ready', video );
return Promise.resolve( video );
}
return new Promise( ( resolve, reject ) => {
+ debug( 'Promising to poll video data' );
setTimeout( () => {
pollingUploadedVideoData( data ).then( resolve ).catch( reject );
}, 2000 );
@@ -265,17 +273,17 @@ const uploadVideo =
async ( { dispatch } ) => {
const tempId = uid();
- // @todo: implement progress and error handler
- const noop = () => {};
-
+ debug( 'Uploading video' );
dispatch( { type: SET_VIDEO_UPLOADING, id: tempId, title: file?.name } );
// @todo: this should be stored in the state
const tokenData = await getMediaToken( 'upload-jwt' );
const onSuccess = async data => {
+ debug( 'Video uploaded', data );
dispatch( { type: SET_VIDEO_PROCESSING, id: tempId, data } );
const video = await pollingUploadedVideoData( data );
+ debug( 'Video processed', video );
dispatch( { type: SET_VIDEO_UPLOADED, video } );
};
@@ -283,10 +291,16 @@ const uploadVideo =
dispatch( { type: SET_VIDEO_UPLOAD_PROGRESS, id: tempId, bytesSent, bytesTotal } );
};
+ const onError = err => {
+ debug( 'upload err handler', err );
+ window.lastErr = err;
+ dispatch( { type: SET_VIDEO_UPLOADING_ERROR, id: tempId, error: err } );
+ };
+
fileUploader( {
tokenData,
file,
- onError: noop,
+ onError,
onProgress,
onSuccess,
} );
diff --git a/projects/packages/videopress/src/client/state/constants.js b/projects/packages/videopress/src/client/state/constants.js
index 43b7ee37a6840..cf14b83bbe20a 100644
--- a/projects/packages/videopress/src/client/state/constants.js
+++ b/projects/packages/videopress/src/client/state/constants.js
@@ -44,6 +44,7 @@ export const FLUSH_DELETED_VIDEOS = 'FLUSH_DELETED_VIDEOS';
export const UPDATE_PAGINATION_AFTER_DELETE = 'UPDATE_PAGINATION_AFTER_DELETE';
export const SET_VIDEO_UPLOADING = 'SET_VIDEO_UPLOADING';
+export const SET_VIDEO_UPLOADING_ERROR = 'SET_VIDEO_UPLOADING_ERROR';
export const SET_VIDEO_PROCESSING = 'SET_VIDEO_PROCESSING';
export const SET_VIDEO_UPLOADED = 'SET_VIDEO_UPLOADED';
export const SET_VIDEO_UPLOAD_PROGRESS = 'SET_VIDEO_UPLOAD_PROGRESS';
diff --git a/projects/packages/videopress/src/client/state/reducers.js b/projects/packages/videopress/src/client/state/reducers.js
index 9cc36efbc436b..83880ebc43774 100644
--- a/projects/packages/videopress/src/client/state/reducers.js
+++ b/projects/packages/videopress/src/client/state/reducers.js
@@ -20,6 +20,7 @@ import {
REMOVE_VIDEO,
DELETE_VIDEO,
SET_VIDEO_UPLOADING,
+ SET_VIDEO_UPLOADING_ERROR,
SET_VIDEO_PROCESSING,
SET_VIDEO_UPLOADED,
SET_IS_FETCHING_PURCHASES,
@@ -424,6 +425,27 @@ const videos = ( state, action ) => {
};
}
+ case SET_VIDEO_UPLOADING_ERROR: {
+ const { id, error } = action;
+ const currentMeta = state?._meta || {};
+ const currentMetaItems = currentMeta?.items || {};
+
+ return {
+ ...state,
+ _meta: {
+ ...currentMeta,
+ items: {
+ ...currentMetaItems,
+ [ id ]: {
+ ...currentMetaItems[ id ],
+ uploading: false,
+ error: JSON.parse( error?.originalResponse?.getBody?.() || '{}' ),
+ },
+ },
+ },
+ };
+ }
+
case SET_VIDEO_PROCESSING: {
const { id, data } = action;
const query = state?.query ?? getDefaultQuery();
diff --git a/projects/packages/videopress/src/client/state/selectors.js b/projects/packages/videopress/src/client/state/selectors.js
index 51b2e7dde4cbc..81aebcc16c316 100644
--- a/projects/packages/videopress/src/client/state/selectors.js
+++ b/projects/packages/videopress/src/client/state/selectors.js
@@ -9,6 +9,13 @@ export const getUploadingVideos = state => {
.filter( item => item.uploading );
};
+export const getUploadErrorVideos = state => {
+ const items = state?.videos?._meta?.items || {};
+ return Object.keys( items || {} )
+ .map( id => ( { ...items[ id ], id } ) )
+ .filter( item => !! item.error );
+};
+
export const getVideosQuery = state => {
return state?.videos?.query;
};
@@ -154,6 +161,7 @@ const selectors = {
isFetchingPlaybackToken,
getVideoPressSettings,
+ getUploadErrorVideos,
};
export default selectors;