/* * Wire * Copyright (C) 2018 Wire Swiss GmbH * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * */ import React, {useCallback, useEffect, useState} from 'react'; import {TabIndex} from '@wireapp/react-ui-kit/lib/types/enums'; import {amplify} from 'amplify'; import cx from 'classnames'; import {container} from 'tsyringe'; import {Button, ButtonVariant, useTimeout} from '@wireapp/react-ui-kit'; import {WebAppEvents} from '@wireapp/webapp-events'; import {RestrictedVideo} from 'Components/asset/RestrictedVideo'; import {EventName} from 'src/script/tracking/EventName'; import {useKoSubscribableChildren} from 'Util/ComponentUtil'; import {t} from 'Util/LocalizerUtil'; import {formatSeconds} from 'Util/TimeUtil'; import {useEffectRef} from 'Util/useEffectRef'; import {MediaButton} from './controls/MediaButton'; import {SeekBar} from './controls/SeekBar'; import {FileAsset} from './FileAssetComponent'; import {AssetUrl, useAssetTransfer} from './useAssetTransfer'; import {AssetRepository} from '../../../../../assets/AssetRepository'; import {AssetTransferState} from '../../../../../assets/AssetTransferState'; import type {ContentMessage} from '../../../../../entity/message/ContentMessage'; import type {FileAsset as FileAssetType} from '../../../../../entity/message/FileAsset'; import {TeamState} from '../../../../../team/TeamState'; interface VideoAssetProps { assetRepository?: AssetRepository; isQuote?: boolean; message: ContentMessage; teamState?: TeamState; isFocusable?: boolean; } const VideoAsset: React.FC = ({ message, isQuote, teamState = container.resolve(TeamState), isFocusable = true, }) => { const asset = message.getFirstAsset() as FileAssetType; const {isObfuscated} = useKoSubscribableChildren(message, ['isObfuscated']); const {preview_resource: assetPreviewResource} = useKoSubscribableChildren(asset, ['preview_resource']); const [videoPlaybackError, setVideoPlaybackError] = useState(false); const [videoTimeRest, setVideoTimeRest] = useState(0); const [videoPreview, setVideoPreview] = useState(); const [videoSrc, setVideoSrc] = useState(); const [videoElement, setVideoElement] = useEffectRef(); const [isVideoLoaded, setIsVideoLoaded] = useState(false); const {isFileSharingReceivingEnabled} = useKoSubscribableChildren(teamState, ['isFileSharingReceivingEnabled']); const [displaySmall, setDisplaySmall] = useState(!!isQuote); const {transferState, isUploading, isPendingUpload, uploadProgress, cancelUpload, getAssetUrl, downloadAsset} = useAssetTransfer(message); const [hideControls, setHideControls] = useState(false); const hideControlsCallback = useCallback(() => setHideControls(true), []); const {removeTimeout, startTimeout} = useTimeout(hideControlsCallback, 2000); useEffect(() => { if (assetPreviewResource && isFileSharingReceivingEnabled) { getAssetUrl(assetPreviewResource).then(setVideoPreview); } return () => { videoPreview?.dispose(); videoSrc?.dispose(); }; }, []); // Initial check if video is supported with `canPlayType` method, which checks for MIME type, e.g. 'video/mp4' or 'video/mov'. // It's not 100% reliable (e.g. doesn't check codecs), but it's synchorous, which is helpful for initial rendering. const isVideoMimeTypeSupported = (mimeType: string): boolean => { const video = document.createElement('video'); const canPlay = video.canPlayType(mimeType) !== ''; if (!canPlay) { amplify.publish(WebAppEvents.ANALYTICS.EVENT, EventName.MESSAGES.VIDEO.PLAY_FAILED); amplify.publish(WebAppEvents.ANALYTICS.EVENT, EventName.MESSAGES.VIDEO.UNSUPPORTED_MIME_TYPE); } return canPlay; }; // Advanced check for video playability. // It's more reliable than `isVideoMimeTypeSupported` (e.g. checks for codecs), but it's async, so it's not suitable for initial rendering. // It's used when user tries to play the video. const isVideoPlayable = async (url: string): Promise => { const video = document.createElement('video'); return new Promise(resolve => { video.onloadedmetadata = () => { // Detects is the video track is properly available. // If these dimensions are 0, typically the video track can't be properly decoded/rendered. if (video.videoWidth === 0 || video.videoHeight === 0) { resolve(false); return; } resolve(true); }; video.onerror = () => resolve(false); video.src = url; }); }; const onPlayButtonClicked = async (): Promise => { if (isFileSharingReceivingEnabled) { setDisplaySmall(false); if (videoSrc && videoElement) { videoElement.play(); } else { asset.status(AssetTransferState.DOWNLOADING); try { const assetUrl = await getAssetUrl(asset.original_resource()); const playable = await isVideoPlayable(assetUrl.url); if (!playable) { amplify.publish(WebAppEvents.ANALYTICS.EVENT, EventName.MESSAGES.VIDEO.PLAY_FAILED); amplify.publish(WebAppEvents.ANALYTICS.EVENT, EventName.MESSAGES.VIDEO.UNPLAYABLE_ERROR); setVideoPlaybackError(true); return; } setVideoSrc(assetUrl); setIsVideoLoaded(true); amplify.publish(WebAppEvents.ANALYTICS.EVENT, EventName.MESSAGES.VIDEO.PLAY_SUCCESS); } catch (error) { setVideoPlaybackError(true); amplify.publish(WebAppEvents.ANALYTICS.EVENT, EventName.MESSAGES.VIDEO.PLAY_FAILED); console.error('Failed to load video asset ', error); } asset.status(AssetTransferState.UPLOADED); } } }; const onPauseButtonClicked = (): void => { videoElement?.pause(); }; const onVideoPlaying = (): void => { if (!videoElement) { return; } videoElement.style.backgroundColor = '#000'; }; useEffect(() => { if (videoSrc && videoElement) { const playPromise = videoElement.play(); playPromise?.catch(error => { console.error('Failed to load video asset ', error); }); } }, [videoElement, videoSrc]); const syncVideoTimeRest = () => { if (videoElement) { setVideoTimeRest(videoElement.duration - videoElement.currentTime); } }; if (isObfuscated) { return null; } if (!isVideoMimeTypeSupported(asset.file_type || '')) { return ; } return (
{!isFileSharingReceivingEnabled ? ( ) : ( <>
{ removeTimeout(); setHideControls(false); }} >
); }; export {VideoAsset};