@fto-consult/expo-ui
Version:
Bibliothèque de composants UI Expo,react-native
479 lines (465 loc) • 19.7 kB
JavaScript
import * as FileSaver from "$efile-system/utils/FileSaver";
import {isElectron} from "$cplatform";
import notify from "$cnotify";
import Preloader from "$preloader";
import {defaultStr,isNonNullString,defaultObj,isDataURL,getFileExtension,isPromise} from "$cutils";
import {HStack} from "$ecomponents/Stack";
import Label from "$ecomponents/Label";
import DialogProvider from "$ecomponents/Form/FormData/DialogProvider";
import session from "$session";
import Image from "$ecomponents/Image";
import View from "$ecomponents/View";
import theme from "$theme";
import APP from "$capp/instance";
import useContext from "$econtext";
import Button from "$ecomponents/Button";
import {ProgressBar} from "react-native-paper";
export const events = {
RECORDING_STARTED : "RECORDING_STARTED",
RECORDING_PAUSED : "RECORDING_PAUSED",
RECORDING_STOPED : "RECORDING_STOPRED",
RECORDING_STATUS : "RECORDING_STATUS",
}
import {useState,useEffect,useMemo,useRef} from "react";
const startSessionKey = "desktop-capturer-session";
const actionsSessionKey = "desktop-capturer-actions";
export const canRecord = x=> isElectron()? true : typeof navigator !=="undefined" && window?.navigator && (navigator?.mediaDevices) && typeof navigator?.mediaDevices?.getDisplayMedia === 'function';
export const updateSystemTray = x => false;
export function getUserMedia(constraints) {
// if Promise-based API is available, use it
if ((navigator?.mediaDevices && typeof navigator?.mediaDevices?.getUserMedia =="function")) {
return navigator.mediaDevices.getUserMedia(constraints);
}
// otherwise try falling back to old, possibly prefixed API...
const legacyApi = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
if (legacyApi) {
return new Promise(function (resolve, reject) {
legacyApi.bind(navigator)(constraints, resolve, reject);
});
}
return Promise.reject({status:false,msg:"user media not available"})
}
export const getAudioConstraint = x=>{
return {audio: true,echoCancellation:{exact: true},noiseSuppression:{exact:true}};
}
export async function getUserMediaAsync(constraints,video) {
try {
const stream = await (video === false ? getUserMedia(constraints): navigator.mediaDevices.getDisplayMedia(constraints));
return stream;
} catch (e) {
console.error('navigator.getUserMedia error:', e);
}
return null;
}
export function handleUserMediaError(e) {
console.error(e," stream recording error");
notify.error(e);
}
export function getSupportedMimeTypes(mediaTypes,filter) {
if(isNonNullString(mediaTypes)){
mediaTypes = mediaTypes.split(",");
}
mediaTypes = Array.isArray(mediaTypes)? mediaTypes : isNonNullString(mediaTypes)? mediaTypes.split(",") : [];
filter = typeof filter =="function"? filter : x=>true;
if (!mediaTypes.length) mediaTypes.push(...['video', 'audio'])
const FILE_EXTENSIONS = ['webm', 'ogg', 'mp4', 'x-matroska']
const CODECS = ['vp9', 'vp9.0', 'vp8', 'vp8.0', 'avc1', 'av1', 'h265', 'h.265', 'h264', 'h.264', 'opus']
return [...new Set(
FILE_EXTENSIONS.flatMap(ext =>
CODECS.flatMap(codec =>
mediaTypes.flatMap(mediaType => [
`${mediaType}/${ext};codecs:${codec}`,
`${mediaType}/${ext};codecs=${codec}`,
`${mediaType}/${ext};codecs:${codec.toUpperCase()}`,
`${mediaType}/${ext};codecs=${codec.toUpperCase()}`,
`${mediaType}/${ext}`,
]),
),
),
)].filter(variation => MediaRecorder.isTypeSupported(variation) && filter(variation))
}
const recordingKeys = ['isRecording','isPaused','isInactive'];
function mainDesktopCapturer (){
let mimeType = "video/webm;codecs=vp9";
let recordingOptions = {};
var recorder;
let blobs = [];
const electronDesktopCapturer = isElectron() && typeof window?.ELECTRON !=="undefined" && typeof window?.ELECTRON?.desktopCapturer =="object" && ELECTRON?.desktopCapturer || {};
const getRecordingStatus = ()=>{
const ret = {}
if(recorder){
recordingKeys.map((v)=>{
if(recorder){
ret[v] = recorder.state == v.toLowerCase().split("is")[1]? true : false
} else {
ret[v] = false;
}
});
} else if(electronDesktopCapturer?.getRecordingStatus){
return electronDesktopCapturer?.getRecordingStatus();
}
return ret;
};
function handleStream(stream,opts) {
opts = Object.assign({},opts);
recorder = new MediaRecorder(stream, { mimeType});
blobs = [];
recorder.ondataavailable = function(event) {
if(event.data.size > 0){
blobs.push(event.data);
}
updateSystemTray();
};
recorder.onstop = function(event){
updateSystemTray();
if(!blobs.length) return false;
const opts = defaultObj(recordingOptions);
let {fileName} = opts;
fileName = defaultStr(fileName,"video-"+APP.getName()+"-"+(new Date().toFormat("dd-mm-yyyy HHMM"))).trim();
fileName = (fileName.rtrim(getFileExtension(fileName,false)))+".webm";
return FileSaver.save({content:new Blob(blobs, {type: mimeType}),mimeType,fileName}).then(({path,fileName})=>{
if(isNonNullString(path) || isNonNullString(fileName)){
notify.info(`Vidéo sauvegardée ${isNonNullString(path)?` à l'emplacement [${path}]`:` avec comme nom de fichier ${fileName}`}`);
}
}).catch(notify.error).finally(()=>{
recorder = undefined;
blobs = [];
});
}
recorder.start(1000);
updateSystemTray();
}
function startRecording(opts) {
if(!canRecord()){
return Promise.reject({stauts:false,isRecording:false,msg:"unable to get user media, get user media is not a function"})
}
if(recorder){
recorder.stop();
}
recorder = undefined;
opts = defaultObj(opts)
if(isNonNullString(opts.mimeType)){
const mimeTypes = getSupportedMimeTypes(x=>!x.startsWith("audio/"))
if(mimeTypes.includes(opts.mimeType)){
mimeType = opts.mimeType;
}
}
const timer = typeof opts.timer =="number"? opts.timer : 0;
const cb = (e)=>{
setTimeout(()=>{
const status = desktopCapturer.getRecordingStatus();
APP.trigger(events.RECORDING_STATUS,status);
},timer*1000+1000);
}
if(typeof electronDesktopCapturer?.startRecording ==='function'){
try {
const eRecorder = electronDesktopCapturer.startRecording({...opts,mimeType,updateSystemTray,handleUserMediaError})
return Promise.resolve(eRecorder).then(cb).catch(notify.error);
} catch(e){
notify.error(e);
return Promise.reject(e);
}
}
opts.video = defaultObj(opts.video);
const audio = isBool(opts.audio) && !opts.audio ? false : defaultObj(opts.audio);
const video = {
...opts.video,
mediaSource: "screen"
}
recordingOptions = opts;
return new Promise((resolve,reject)=>{
if(audio){
(async() => {
const audioStream = await getUserMediaAsync(getAudioConstraint(),false)
const videoStream = await getUserMediaAsync({audio:false,video})
if(audioStream && videoStream){
const combinedStream = new MediaStream([...videoStream.getVideoTracks(), ...audioStream.getAudioTracks()])
handleStream(combinedStream,opts)
}
resolve({isRecording:true});
})();
} else {
return getUserMediaSync({audio:false,video}).then((stream)=>{
handleStream(stream,opts);
cb();
resolve({isRecording:true})
}).catch(handleUserMediaError);
}
return resolve({isRecording:false});
})
}
function pauseRecording(){
if(electronDesktopCapturer?.pauseRecording){
return electronDesktopCapturer?.pauseRecording();
}
if(!recorder || !getRecordingStatus().isRecording) return;
recorder.pause();
updateSystemTray();
return true;
}
function resumeRecording(){
if(electronDesktopCapturer?.resumeRecording) return electronDesktopCapturer.resumeRecording();
if(!recorder || !getRecordingStatus().isPaused) return;
recorder.resume();
updateSystemTray();
return true;
}
function stopRecording(opts) {
setTimeout(()=>{
const status = desktopCapturer.getRecordingStatus();
APP.trigger(events.RECORDING_STATUS,status);
},1000);
if(electronDesktopCapturer.stopRecording) return electronDesktopCapturer.stopRecording();
if(!recorder) return false;
let s = getRecordingStatus();
if(!s.isPaused && !s.isRecording){
recorder = undefined;
return false;
}
if(recorder){
let s = getRecordingStatus();
if(s.isRecording || s.isPaused){
recorder.stop();
}
}
recorder = undefined;
return true;
}
return {
canRecord,
isUserMediaAvailable:canRecord,
getRecordingStatus,
startRecording,
pauseRecording,
resumeRecording,
getAudioConstraint,
stopRecording,
getSupportedMimeTypes,
electron : electronDesktopCapturer,
isElectron : typeof electronDesktopCapturer.getRecordingStatus ==="function",
}
}
export const looopForTimer = (timer)=>{
return new Promise((resolve,reject)=>{
let timerCB = undefined;
timer = typeof timer =='number'? timer : 3000;
const loopCB = ()=>{
clearTimeout(timerCB);
const d = Math.ceil(timer/1000);
const testID = "RN_PreloaderLooper";
if(timer >= 1000){
Preloader.open({
content : <HStack testID={testID}>
<Label>Début capture dans</Label>
<Label textBold fontSize={40}>{" "+(d).formatNumber()+" "}</Label>
<Label>seconde{d>1 &&"s"}</Label>
</HStack>
})
timer-=1000;
timerCB = setTimeout(loopCB,900);
return;
}
Preloader.close();
resolve();
}
return loopCB();
})
}
const desktopCapturer = mainDesktopCapturer();
export async function handleCapture(){
if(!canRecord()){
const message = "Impossible de faire des enregistrements vidéo sur ce type de périférique";
notify.error(message);
return Promise.reject({message});
}
const {isRecording,isPaused} = desktopCapturer.getRecordingStatus();
let fields = {},title = "Capture d'écran vidéo en cours", yes = null, no = "Annuler";
const mimeTypes = getSupportedMimeTypes("video");
let onSuccess = undefined;
const sKey = !isPaused && !isRecording ? startSessionKey : actionsSessionKey;
const data = Object.assign({},session.get(sKey));
if(!isPaused && !isRecording){
let sources = null;
const {electron} = desktopCapturer;
const screenAccess = typeof electron.getScreenAccess =="function" && electron.getScreenAccess() || true;
if(!screenAccess){
const msg = `Le partage d'écran n'est pas activé sur votre système, merci d'activer le partage d'écran dans les paramètres systèmes`;
notify.error(msg);
return Promise.reject({message:msg});
}
if(typeof electron.getSources =='function'){
sources = await electron.getSources();
}
title = "Effectuer une capture d'écran vidéo";
fields = {
source : Array.isArray(sources)? {
text : "Sélectionner la source de l'écran à capturer",
type : "select",
required : true,
items : sources,
itemValue : ({item,index})=>item.id,
//compare : (a,b)=> a?.id === b?.id,
listProps : {itemHeight:200},
itemHeight : 250,
renderItem : ({item,index})=>{
if(!isObj(item) || !isDataURL(item.thumbnailURL)) return null;
return <View style={[theme.styles.w100,theme.styles.alignItemsFlexStart]} testID = {`RNViewSource_${item.id}`}>
<Label textBold primary>{item.name}</Label>
<Image editable={false} width = {250} rounded = {false} src={item.thumbnailURL}/>
</View>
},
renderText : ({item,index})=>{
return `${item?.name}`;
}
} : undefined,
audio : {
text : "Enregistrer le son",
type : 'switch',
defaultValue : true,
checkedValue : true,
uncheckedValue : false,
},
timer : {
text : 'Délai d\'attente en secondes',
type : 'number',
format : 'number',
defaultValue : !isElectron() ? 0 : 3,
},
mimeType : {
text : 'Format de la vidéo',
type : 'select',
items : mimeTypes,
defaultValue : mimeTypes[0],
itemValue : ({item,index})=>item,
renderText : ({item,index}) => item,
renderItem : ({item,index}) => item,
},
showPreloaderOnScreenCapture : electron?.isElectron ? {
text : "Afficher la progression",
tooltip : "Afficher la progression au niveau de l'icone de l'application",
defaultValue : 1,
} : undefined
}
yes = {
text : 'Capturer',
icon : "record"
}
onSuccess = ({data})=>{
const timer = Math.ceil(typeof data.timer =="number"? data.timer : 0);
if(timer > 0){
return looopForTimer(timer*1000).then(()=>{
desktopCapturer.startRecording(data);
});
}
desktopCapturer.startRecording(data);
}
} else {
const type = isRecording || isPaused ? "radio" : undefined;
fields = {
action : {
text : 'Que voulez vous faire?',
type : 'select',
items : [
isRecording? {
code :'pauseRecording',
label:'Mettre la capture en pause',
type,
} : undefined,
isPaused ? {
code :"resumeRecording",
label:'Reprendre la capture vidéo',
type,
} : undefined,
{
code:'stopRecording',
label:'Arréter la capture vidéo',
},
],
defaultValue : isPaused?'resumeRecording' : 'stopRecording',
multiple : false,
required : true,
}
}
yes = {
text : "Exécuter",
icon : "play",
}
no = {
text : "Annuler",
icon :"cancel",
}
onSuccess = ({data})=>{
if(typeof desktopCapturer[data.action] ==='function'){
return desktopCapturer[data.action]();
}
}
}
return DialogProvider.open({
title,
actions : [yes],
onSuccess : ({data,...rest})=>{
if(onSuccess) onSuccess({data,...rest});
DialogProvider.close();
session.set(sKey,data);
},
data,
fields,
});
}
export const useRecordingStatus = ()=>{
const [status,setStatus] = useState(desktopCapturer.getRecordingStatus());
const timerRef = useRef(null);
const {isPaused,isRecording,isInactive} = status;
const mRecord = canRecord();
useEffect(()=>{
if(!mRecord) return ()=>{};
if(isInactive){
clearInterval(timerRef.current);
timerRef.current = null;
} else {
timerRef.current = setInterval(()=>{
const nStatus = desktopCapturer.getRecordingStatus();
if(nStatus.isPaused !== status.isPaused || nStatus.isInactive !== status.isInactive || nStatus.isRecording !== status.isRecording){
setStatus({...nStatus});
}
},1000);
}
return ()=>{
clearInterval(timerRef.current);
timerRef.current = null;
}
},[isPaused,isRecording,isInactive]);
useEffect(()=>{
return ()=>{
clearInterval(timerRef.current);
timerRef.current = null;
}
},[]);
return status;
}
/**** le bouton permettant d'exécuter la fonction de recording de l'application */
export const RecordingButton = function({onPress,testID,...props}){
const {desktopCapturer:dParams} = useContext();
const {isRecording,isPaused,isInactive} = useRecordingStatus();
const canCaptureDesktop = !canRecord()? false : typeof dParams =="function"? !!dParams() : typeof dParams =="boolean"? dParams : true;
if(!canCaptureDesktop) return null;
testID = defaultStr(props.testID,"RNScreenRecordStatusButton")
const color = theme.Colors.setAlpha(theme.colors.text,theme.ALPHA);
const buttonContainerProps = {style:[theme.styles.w100,theme.styles.alignItemsFlexStart]}
return <View testID={testID+"_Container"} {...buttonContainerProps}>
<Button forceWhiteColorOnDarkMode={false} containerProps = {buttonContainerProps} {...props} style={[{color},buttonContainerProps.style,props.style]} upperCase={false} onPress = {(...args)=>{
if(onPress && onPress(...args) === false) return;
handleCapture();
}}
icon={isPaused ? "material-pause-presentation":isRecording ? "record-rec":"record"} iconProps={{color}} testID={testID}
>
{isPaused ? "Capture vidéo en pause" : isRecording ? "Capture vidéo en cours ..." :"Faire une capture vidéo" }
</Button>
{isPaused || isRecording ? <ProgressBar
indeterminate = {isRecording || undefined}
color = {theme.colors.primary}
progress = {isPaused ? 50 : undefined}
/> : null}
</View>
}
export default desktopCapturer;