Live stream HTML5 video draw-to-canvas not working - reactjs

I use ReactJS to display a live stream (from my webcam) using the HTML5 video element. The OpenVidu media server handles the backend.
I would like to use the canvas element to draw the video live stream onto the canvas using the drawImage() method.
I have seen other examples, but in them the video element always has a source. Mine does not have a source - when I inspect the video element all I see is:
<video autoplay id="remote-video-zfrstyztfhbojsoc_CAMERA_ZCBRG"/>
This is what I have tried, however the canvas does not work.
export default function Video({ streamManager }) {
const videoRef = createRef()
const canvasRef = createRef()
useEffect(() => {
if (streamManager && !!videoRef) {
//OpenVidu media server attaches the live stream to the video element
streamManager.addVideoElement(videoRef.current)
if (canvasRef.current) {
let ctx = canvasRef.current.getContext('2d')
ctx.drawImage(videoRef.current, 0, 0)
}
}
})
return (
<>
//video element displays the live stream
<video autoPlay={true} ref={videoRef} />
// canvas element NOT working, nothing can be seen on screen
<canvas autoplay={true} ref={canvasRef} width="250" height="250" />
</>
)
}
UPDATE: after further investigation I realised I needed to use the setInterval() function and therefore provided the solution below.

The solution is to extract the canvas logic in a self contained component, and use a setInterval() so that it will draw the video element on the canvas, every 100 ms (or as needed).
Video Component
import React, { useEffect, createRef } from 'react'
import Canvas from './Canvas'
export default function Video({ streamManager }) {
const videoRef = createRef()
useEffect(() => {
if (streamManager && !!videoRef) {
//OpenVidu media server attaches the live stream to the video element
streamManager.addVideoElement(videoRef.current)
}
})
return (
<>
//video element displays the live stream
<video autoPlay={true} ref={videoRef} />
//extract canvas logic in new component
<Canvas videoRef={videoRef} />
</>
)
}
Canvas Component
import React, { createRef, useEffect } from 'react'
export default function Canvas({ videoRef }) {
const canvasRef = createRef(null)
useEffect(() => {
if (canvasRef.current && videoRef.current) {
const interval = setInterval(() => {
const ctx = canvasRef.current.getContext('2d')
ctx.drawImage(videoRef.current, 0, 0, 250, 188)
}, 100)
return () => clearInterval(interval)
}
})
return (
<canvas ref={canvasRef} width="250" height="188" />
)
}

You are using useEffect without parameter which mean your useEffect will call in every render
useEffect(() => {
// every render
})
If you want to run your drawImage at only mount time then use useEffect with []
useEffect(() => {
// run at component mount
},[])
Or if you want to run drawImage when any parameter change then pass your parameter to it
useEffect(() => {
// run when your streamManager change
},[streamManager])
use it as per your requirement

Related

Why lottieRef returns only null?

Using the library "# lottiefiles/react-lottie-player"
You need to get lottieRef to interact with animation, but I get null.
Code reference: https://codesandbox.io/s/great-rgb-7dp4j0?file=/src/HorizontalPicker/HorizontalPicker.jsx
import React, {useEffect, useRef} from "react";
import {Player} from "#lottiefiles/react-lottie-player";
export default function App() {
const player = useRef(null)
const lottie = useRef(null)
useEffect(() => {
if(lottie && lottie.current){
console.log(lottie.current) //return null
}
}, [])
return (
<div className="App">
<Player
lottieRef={data => lottie.current = data}
ref={player}
onEvent={event =>{
if(event === "load"){
lottie.current.play() //nothing
}
}}
keepLastFrame={true}
autoplay={false}
loop={true}
src={"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"}
style={{width: "100%", height: "2.5em", padding: "0", margin: "0"}}/>
</div>
);
}
There is also an interesting point.
If you output lottie, we get an object with null (while there is something inside it)
And if you output lottie.current, we get null.
Reference to an example of this thing: https://ibb.co/RQWxLkJ
Can you pass the lottie ref into your ref like the following (see ref on the div):
export default function App() {
const player = useRef(null)
const lottie = useRef(null)
useEffect(() => {
if(lottie && lottie.current){
console.log(lottie.current) //return null
}
}, [])
return (
<div className="App">
<Player
lottieRef={data => lottie.current = data}
ref={player}
onEvent={event =>{
if(event === "load"){
lottie.current && lottie.current.play() //nothing
}
}}
keepLastFrame={true}
autoplay={false}
loop={true}
src={"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"}
style={{width: "100%", height: "2.5em", padding: "0", margin: "0"}}/>
</div>
);
}
This way I am able to get the animation object when I console lottie.current
I'd prefer to use useState to save the animationData rather than a ref. But there was also an issue on the player where the 'load' event was firing but the player hadn't finish setting its internal instance of the animation therefor calling play() wouldn't work. This was happening only on functional components in React that's why it went undiscovered.
I've made a few changes to your code and to the player, using v1.5.2 you should be able to accomplish what you're looking for:
import css from "./HorizontalPicker.module.css";
import React, { useRef, useState, useEffect } from "react";
import { Player } from "#lottiefiles/react-lottie-player";
const HorizontalPicker = () => {
const player = useRef(null);
// const lottie = useRef(null);
const [lottie, setLottie] = useState(null);
useEffect(() => {
if (lottie) {
console.log(" Lottie animation data : ");
console.log(lottie);
// You can also play by calling the underlying lottie method
// lottie.play();
}
}, [lottie]);
return (
<>
<Player
onEvent={(event) => {
// console.log(event);
if (event === "instanceSaved" && player && player.current) {
console.log("Playing animation..");
player.current.play();
}
}}
lottieRef={(data) => {
setLottie(data);
}}
ref={player}
keepLastFrame={true}
autoplay={false}
loop={true}
src={
"https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json"
}
/>
</>
);
};
export default HorizontalPicker;
Sandbox:
https://codesandbox.io/s/lottie-react-functional-component-2bs8fs?file=/src/HorizontalPicker/HorizontalPicker.jsx
Cheers!
Well, according to official documentation https://github.com/LottieFiles/lottie-react, lottieRef represents a callback function which is fired by Player component (and this function returns AnimationFrame object)
I'm not familiar with this library, and whatever I'll describe next are just my assumptions :) Seems that whenever player "plays", it displays frames one-by-one (from json file in "src" attribute"). And whenever player displays frames from .json file - Player fires "lottieRef" event which you utilize to set lottie.current. And player starts displaying frames only when it loads .json data using "src" parameter in Player definition (see network tab in your browser to ensure that additional http request presents)
And in this case everything seems pretty logical: you try to access "lottie" variable in useEffect of your component but it's empty as far as Player did not manage to display any frame yet because the callback (lottieRef) did not fire yet as far as .json file is not loaded yet. No matter if .json file is large or small, Player requests it via additional http call, which requires some (even minital) amount time. And useEffect fires before .json is loaded immediately after rendering DOM (that's how ReactJS works)
On the other hand, if you delay a bit request to "lottie" ref - you'll see that it is populated:
Code example:
import React, { useEffect, useRef } from 'react';
import { Player } from '#lottiefiles/react-lottie-player';
export default function App() {
const player = useRef(null);
const lottie = useRef(null);
useEffect(() => {
setTimeout(() => console.log(lottie), 50);
}, []);
return (
<div className="App">
<Player
lottieRef={(data) => (lottie.current = data)}
ref={player}
keepLastFrame={true}
autoplay={false}
loop={true}
src={
'https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json'
}
/>
</div>
);
}
So if you delay onEvent callback event a bit, "lottie" ref would be initialized by that moment and .play() would work:
onEvent={(event) => {
event === 'load' &&
setTimeout(
() => lottie.current && lottie.current.play()
);
}
}
BUT, if the only purpose you have is to execute Player whenever it's ready - why not to use "autoplay" built-in property ? (autoplay={true})
import React, { useRef } from 'react';
import { Player } from '#lottiefiles/react-lottie-player';
export default function App() {
const player = useRef(null);
const lottie = useRef(null);
return (
<div className="App">
<Player
lottieRef={(data) => (lottie.current = data)}
ref={player}
keepLastFrame={true}
autoplay={true}
loop={true}
src={
'https://lottie.host/2c01fd6c-437d-494e-af27-2a37e322bc60/prXv4Ic6px.json'
}
/>
</div>
);
}
Hope it'll help

Why do I get error when I use forwardRef in react-native?

I just try to understand how to use this property.
In my main file, I added these:
import Audio from "../../../src/Audio";
const audioRef = useRef(null);
useEffect(()=> {
audioRef.current.play();
}, [])
under return:
<View>
<Audio ref={audioRef} />
</View>
And here is Audio.js
import React from "react";
import Song from "./components/song/song.mp3";
const Audio = React.forwardRef((props,ref) => {
return(
<audio src={Song} ref={ref}></audio>
)
})
export default Audio;
here is the error,
It means you need an additional render cycle before the ref is defined and attached to the DOMNode to call the play function of. You can add a "initialRender" state and trigger the effect on the second render.
const audioRef = useRef(null);
const [initialRender, setInitialRender] = useState(true);
useEffect(() => {
setInitialRender(false);
}, []);
useEffect(()=> {
if (!initialRender) {
audioRef.current.play();
}
}, [initialRender]);
You should receive the error as you might violating the rules for react component.
as in the Audio component, you are using another component that start's with audio which starts with the small case as you should have to use the component name that starts with a capital letter.
Have a look at the error code.
const Audio = React.forwardRef((props,ref) => {
return(
// error is here component name must starts with a capital letter
<audio src={Song} ref={ref}></audio>
)
})

React, insert/append/render existing <HTMLCanvasElement>

In my <App> Context, I have a canvas element (#offScreen) that is already hooked in the requestAnimationFrame loop and appropriately drawing to that canvas, verified by .captureStream to a <video> element.
In my <Canvas> react component, I have the following code (which works, but seems clunky/not the best way to copy an offscreen canvas to the DOM):
NOTE: master is the data object for the <App> Context.
function Canvas({ master, ...rest } = {}) {
const canvasRef = useRef(master.canvas);
const draw = ctx => {
ctx.drawImage(master.canvas, 0, 0);
};
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext("2d");
let animationFrameId;
const render = () => {
draw(ctx)
animationFrameId = window.requestAnimationFrame(render)
}
render();
return () => {
window.cancelAnimationFrame(animationFrameId);
}
}, [ draw ]);
return (
<canvas
ref={ canvasRef }
onMouseDown={ e => console.log(master, e) }
/>
);
};
Edited for clarity based on comments
In my attempts to render the master.canvas directly (e.g. return master.canvas; in <Canvas>), I get some variation of the error "Objects cannot be React children" or I get [object HTMLCanvasElement] verbatim on the screen.
It feels redundant to take the #offScreen canvas and repaint it each frame. Is there, instead, a way to insert or append #offScreen into <Canvas>, so that react is just directly utilizing #offScreen without having to repaint it into the react component canvas via the ref?
Specific Issue: Functionally, I'm rendering a canvas twice--once off screen and once in the react component. How do I (replace/append?) the component's <canvas> element with the offscreen canvas (#offScreen), instead of repainting it like I'm doing now?
For anyone interested, this was actually fairly straightforward, as I overcomplicated it substantially.
export function Canvas({ canvas, ...rest }) {
const container = useRef(null);
useEffect(() => {
container.current.innerHTML = "";
container.current.append(canvas);
}, [ container, canvas ]);
return (
<div ref={ container } />
)
}

Tooltip delay on hover with RXJS

I'm trying to add tooltip delay (300msemphasized text) using rxjs (without setTimeout()). My goal is to have this logic inside of TooltipPopover component which will be later be reused and delay will be passed (if needed) as a prop.
I'm not sure how can I add "delay" logic inside of TooltipPopover component using rxjs?
Portal.js
const Portal = ({ children }) => {
const mount = document.getElementById("portal-root");
const el = document.createElement("div");
useEffect(() => {
mount.appendChild(el);
return () => mount.removeChild(el);
}, [el, mount]);
return createPortal(children, el);
};
export default Portal;
TooltipPopover.js
import React from "react";
const TooltipPopover = ({ delay??? }) => {
return (
<div className="ant-popover-title">Title</div>
<div className="ant-popover-inner-content">{children}</div>
);
};
App.js
const App = () => {
return (
<Portal>
<TooltipPopover>
<div>
Content...
</div>
</TooltipPopover>
</Portal>
);
};
Then, I'm rendering TooltipPopover in different places:
ReactDOM.render(<TooltipPopover delay={1000}>
<SomeChildComponent/>
</TooltipPopover>, rootEl)
Here would be my approach:
mouseenter$.pipe(
// by default, the tooltip is not shown
startWith(CLOSE_TOOLTIP),
switchMap(
() => concat(timer(300), NEVER).pipe(
mapTo(SHOW_TOOLTIP),
takeUntil(mouseleave$),
endWith(CLOSE_TOOLTIP),
),
),
distinctUntilChanged(),
)
I'm not very familiar with best practices in React with RxJS, but this would be my reasoning. So, the flow would be this:
on mouseenter$, start the timer. concat(timer(300), NEVER) is used because although after 300ms the tooltip should be shown, we only want to hide it when mouseleave$ emits.
after 300ms, the tooltip is shown and will be closed mouseleave$
if mouseleave$ emits before 300ms pass, the CLOSE_TOOLTIP will emit, but you could avoid(I think) unnecessary re-renders with the help of distinctUntilChanged

How can I reset a dragged component to its original position with react-draggable?

I try to implement a function in my app that allows the user to reset all the components that he dragged around to be reset to their original position.
I assume that this functionality exists in react-draggable because of this closed and released issue: "Allow reset of dragging position" (https://github.com/idanen/react-draggable/issues/7). However I did not find any hint in the documentation (https://www.npmjs.com/package/react-draggable).
There was one question with the same content in stackoverflow, but it has been removed (https://stackoverflow.com/questions/61593112/how-to-reset-to-default-position-react-draggable).
Thanks for your help :-)
The referenced issue on the GitHub references a commit. After taking a look at the changes made in this commit, I found a resetState callback added to the useDraggable hook. In another place in the commit, I found a change to the test file which shows usage of the hook.
function Consumer(props) {
const {
targetRef,
handleRef,
getTargetProps,
resetState,
delta,
dragging
} = useDraggable(props);
const { style = defaultStyle } = props;
return (
<main
className='container'
ref={targetRef}
data-testid='main'
style={style}
{...getTargetProps()}
>
{dragging && <span>Dragging to:</span>}
<output>
{delta.x}, {delta.y}
</output>
<button className='handle' ref={handleRef}>
handle
</button>
<button onClick={resetState}>reset</button>
</main>
);
}
The hook returns a set of callbacks, including this callback, which can be used to reset the state of the draggable.
I wanted the component to reset back to its original position when the component was dropped.
Using hooks I monitored if the component was being dragged and when it was false reset the position otherwise it would be undefined.
export default function DraggableComponent(props: any) {
const {label} = props
const [isDragging, setIsDragging] = useState<boolean>(false)
const handleStart = (event: any, info: DraggableData) => {
setIsDragging(true)
}
const handleStop = (event: any, info: DraggableData) => {
setIsDragging(false)
}
return (
<Draggable
onStart={handleStart}
onStop={handleStop}
position={!isDragging? { x: 0, y: 0 } : undefined}
>
<Item>
{label}
</Item>
</Draggable>
)
}
Simple approach would be:
creating a new component to wrap our functionality around the Draggable callbacks
reset position when onStop callback is triggered
Example:
import { useState } from 'react';
import Draggable, { DraggableData, DraggableEvent, DraggableProps } from 'react-draggable';
export function Drag({ children, onStop, ...rest }: Partial<DraggableProps>) {
const initial = { x: 0, y: 0 }
const [pos, setPos] = useState(initial)
function _onStop(e: DraggableEvent, data: DraggableData){
setPos(initial)
onStop?.(e, data)
}
return (
<Draggable position={pos} onStop={_onStop} {...rest}>
{children}
</Draggable>
)
}
Usage:
export function App() {
return (
<Drag> Drag me </Drag>
)
}
Note that this answer does not work.
None of these approaches worked for me, but tobi2424's post on issue 214 of the Draggable repo did. Here's a minimal proof-of-concept:
import React from "react";
import Draggable from "react-draggable";
const DragComponent = () => {
// Updates the drag position parameter passed to Draggable
const [dragPosition, setDragPosition] = React.useState(null);
// Fires when the user stops dragging the element
const choiceHandler = () => {
setDragPosition({x: 0, y: 0});
};
return (
<Draggable
onStop={choiceHandler}
position={dragPosition}
>
Drag me
</Draggable>
);
};
export default DragComponent;
Edit
The code above works intermittently but not particularly well. As far as I can work out, react-draggable stores data about the position of the dragged element somewhere outside of React, in order to preserve the position of the element between component refreshes. I was unable to determine how to reset the position of the element on command and none of the other example code solves the problem for me.
You can do this in a very haphazard manner. There may be another way to set state more safely on this but I didn't look too deeply into it.
import React from 'react';
export default class 😊 extends Component {
constructor(props) {
super(props);
this.draggableEntity = React.createRef();
}
resetDraggable() {
try {
this.draggableEntity.current.state.x = 0;
this.draggableEntity.current.state.y = 0;
} catch (err) {
// Fail silently
}
}
render() {
return (
<Draggable
ref={this.draggableEntity}
>
<img onClick={(e) => {this.resetDraggable()}}></img>
</Draggable>
)
}
}
There happens to be another way! You can use it's exposed ref element to reset its offset. This can be achieved like so:
import React, {useRef, useCallback} from "react";
import Draggable from "react-draggable";
const DragComponent = () => {
// Updates the drag position parameter passed to Draggable
const [dragPosition, setDragPosition] = React.useState(null);
const draggerRef = useRef(null);
// Fires when the user stops dragging the element
const resetDrag = useCallback(() => {
setDragPosition({x: 0, y: 0});
draggerRef.current?.setState({ x: 0, y: 0 }); // This is what resets it!
}, [setDragPosition, draggerRef]);
return (
<Draggable
ref={draggerRef}
onStop={resetDrag}
position={dragPosition}
>
Drag me
</Draggable>
);
};
export default DragComponent;

Resources