I am trying to get a webcam feed to display on my app using react hooks. I also need to be able to capture the latest image from the feed
I believe I have the foundations but am missing something.
import React,{useState,useEffect} from "react"
export function VideoFeed(){
const[constraints] = useState({width:300,height:300})
useEffect(()=>{
navigator.mediaDevices.getUserMedia({video:true})
.then(stream=>{
let video = document.querySelector('video')
video.source = stream;
video.play();
})
.catch(e=>{
console.log(e)
})
})
return(
<video autoPlay ={true} id ="video"></video>
)
}
See How to access a DOM element in React? instead of document.querySelector.
When applied with useRef hook and fixing how often useEffect needs to execute, it would look something like this:
export function VideoFeed() {
const videoEl = useRef(null)
useEffect(() => {
if (!videoEl) {
return
}
navigator.mediaDevices.getUserMedia({video:true})
.then(stream => {
let video = videoEl.current
video.srcObject = stream
video.play()
})
}, [videoEl])
return <video ref={videoEl} />
}
Found the issue.
Change
video.source = stream;
To:
video.srcObject = stream;
Viola
Related
At the moment, I am working on a project that requires me to add three videos to the homepage, but loading them all at once will reduce the load time considerably.
Also i want to use <video/> tag instead of using <iframe/> because i want that autoplay functionality.
What's the best way to do this in React? Using NextJS and Chakra UI.
You can use IntersectionObserver and do it as below. For React all you have to do is to add the below code in an useEffect with empty dependency.
const video = document.querySelector("video");
function handleIntersection(entries) {
entries.map(async (entry) => {
if (entry.isIntersecting) {
const res = await fetch("/video.mp4");
const data = await res.blob();
video.src = URL.createObjectURL(data);
}
});
}
const observer = new IntersectionObserver(handleIntersection);
observer.observe(video);
<video autoplay muted loop playsinline></video>
Also I used a video with a relative path to avoid possible CORS issues.
i found a way to do it using '#react-hook/intersection-observer'
import useIntersectionObserver from '#react-hook/intersection-observer'
import { useRef } from 'react'
const LazyIframe = () => {
const containerRef = useRef()
const lockRef = useRef(false)
const { isIntersecting } = useIntersectionObserver(containerRef)
if (isIntersecting) {
lockRef.current = true
}
return (
<div ref={containerRef}>
{lockRef.current && (
<video
src={"add video source here"}
type="video/mp4"
></video>
)}
</div>
)
}
I'm looking to play an audio file using custom controls to trigger the .play() method on a custom button. To be clear, I'm not trying to have the audio auto-play. Everything works perfectly in Chrome, but in Safari I get the error:
Unhandled Promise Rejection: NotAllowedError: The request is not
allowed by the user agent or the platform in the current context,
possibly because the user denied permission.
The project is built using React and React Router, so I'm wondering if it's possibly fixable in my useEffect() hook. I've tried enabling controls on the audio element and using CSS to remove them from the DOM, but no luck.
import React, { useState, useRef, useEffect } from "react";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import { gsap } from "gsap";
function RadioPlayerNav(props) {
const audioEl = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const playingTitle = document.querySelector(".radio-player-nav .title p");
const toPX = (value) => {
return (parseFloat(value) / 100) * (/vh/gi.test(value) ? window.innerHeight : window.innerWidth);
};
const radioPlayerGSAP = gsap.to(".radio-player-nav .title p", {
x: toPX("-5vw"),
duration: 4,
ease: "none",
yoyo: true,
repeat: -1,
delay: 1,
repeatDelay: 1,
paused: true,
});
useEffect(() => {
if (isPlaying) {
audioEl.current.play();
radioPlayerGSAP.play();
// radioPlayerGSAP.reversed(4, false);
} else {
audioEl.current.pause();
}
}, [isPlaying]);
return (
<div className="radio-player-nav">
<div className="radio-player-controls">
<audio src="src/current-radio-mix.mp3" ref={audioEl} preload="auto"></audio>
<i
className={isPlaying ? "fas fa-pause cursor-hover" : "fas fa-play cursor-hover"}
onClick={() => {
setIsPlaying(!isPlaying);
}}
></i>
<div className="title">
<p>MIXED FEELINGS M0001</p>
</div>
<a href="src/current-radio-mix.mp3" download="Mixed Feelings M0001">
<i className="fas fa-download cursor-hover"></i>
</a>
</div>
</div>
);
}
export default RadioPlayerNav;
You can find the full github repo for the project here: https://github.com/nallstott/mixed-feelings/tree/master
Turns out, safari requires you to use useLayoutEffect instead of useEffect to accomplish this. I'm leaving the post up since I didn't see anything previously that gave the answer, along with the article that solved it for me in case anyone else has this issue with <audio> on safari.
https://lukecod.es/2020/08/27/ios-cant-play-youtube-via-react-useeffect/
I also play audio in my app, and it cycles through them. I was able to get around this by placing my audio files in a map, and use a separate function to call the audio to play.
import hold_female from '../../assets/audio/Female/hold_female.mp3';
import exhale_female from '../../assets/audio/Female/exhale_female.mp3';
import inhale_female from '../../assets/audio/Female/inhale_female.mp3';
import hold_male from '../../assets/audio/Male/hold_male.mp3';
import exhale_male from '../../assets/audio/Male/exhale_male.mp3';
import inhale_male from '../../assets/audio/Male/inhale_male.mp3';
//Props here...
createAudio('Exhale_female', exhale_female); //These place the audio into a map under the name provided.
createAudio('Inhale_female', inhale_female);
createAudio('Hold_female', hold_female);
createAudio('Exhale_male', exhale_male);
createAudio('Inhale_male', inhale_male);
createAudio('Hold_male', hold_male);
const BreatheTest: FC<BreathingProps> = ({ gender }) => {
const [stageText, setStageText] = useState<string>('Inhale');
const [index, setIndex] = useState<number>(0);
const [milliseconds, setMilliseconds] = useState<number>(0); //Set to 0 so the audio plays right away and there is no delay.
const captions = ['Inhale', 'Hold', 'Exhale', 'Hold'];
const playAudioFiles = () => {
playAudio(`${stageText}_${gender}`);
};
useEffect(() => {
const timeout = setTimeout(() => {
stopAll(); //stop all the previous audio files if they are running.
setStageText(captions[index]);
setIndex(index === 3 ? 0 : index + 1);
setMilliseconds(isSafari ? 4500 : 4350);//Sets the timeout to the time of the audio files.
playAudioFiles(); //Plays the audio files as the useEffect runs
}, milliseconds);
return () => clearTimeout(timeout);
}, [index]);
//... render method and everything else.
}
My app is for controlling breathing, and this is how I have gotten past the error you are seeing. From what I have read, iOS just requires some kind of trigger to start any media, audio or video. Putting the play function into a series of play functions kind of satisfies Safari.
It may not work for you, or how your code works, but if this is where we can discuss how we got around iOS's audio control, this is another way.
I'm using Twillio JS API in my project to display video outputs from multiple sources. This API generates enumeration of DOM video/audio elements that can be attached to the page as follows:
let tracks = TwillioVideo.createLocalTracks({
video: { deviceId: this.state.selectedVideoInput.deviceId },
audio: { deviceId: this.state.selectedAudioInput.deviceId }
}
//Find dom element to attach tracks to
let previewContainer = document.getElementById('local-media')
//Attach all tracks
this.setState({localTracks: tracks})
tracks.forEach(track => previewContainer.appendChild(track.attach()))
track.attach() generates a dom element that can be appended but its not something i can put in React state so it can be rendered like so:
<div id="local-media">{this.state.localTracks.map(track => track.attach()}</div>
If I in fact try to do it i get:
Unhandled Rejection (Invariant Violation): Objects are not valid as a
React child (found: [object HTMLAudioElement]). If you meant to render
a collection of children, use an array instead.
EDIT 1:
I was able to get rid of error by doing this:
{this.state.localTracks.map(track => track.attach().Element)}
but it's not returning renderable html but undefined instead
Twilio developer evangelist here.
The attach method in Twilio Video can take an argument, which is an HTMLMediaElement, and will attach the media to that element.
I would recommend that you create a component you can use to render the media for each media track and then use React refs to get a pointer to the DOM element.
Something like this:
import React, { Component, createRef } from 'react';
class Participant extends Component {
constructor(props) {
super(props);
this.video = createRef();
this.audio = createRef();
this.trackAdded = this.trackAdded.bind(this);
}
trackAdded(track) {
if (track.kind === 'video') {
track.attach(this.video.current);
} else if (track.kind === 'audio') {
track.attach(this.audio.current);
}
}
componentDidMount() {
const videoTrack = Array.from(
this.props.participant.videoTracks.values()
)[0];
if (videoTrack) {
videoTrack.attach(this.video.current);
}
const audioTrack = Array.from(
this.props.participant.audioTracks.values()
)[0];
if (audioTrack) {
audioTrack.attach(this.audio.current);
}
this.props.participant.on('trackAdded', this.trackAdded);
}
render() {
return (
<div className="participant">
<h3>{this.props.participant.identity}</h3>
<video ref={this.video} autoPlay={true} muted={true} />
<audio ref={this.audio} autoPlay={true} muted={true} />
</div>
);
}
}
export default Participant;
Then, for every participant in your chat, you can render one of these component.
Let me know if this helps at all.
I'm trying to show an image when the Chrome browser is offline, and when it's online show the webpage.
I transferred the image to base64 data and tried to load it in the img tag, however the base64 data is too large.
Is there a way to show an image when the browser is offline?
import imageToBase64 from "image-to-base64";
const Home = () => {
const [isOnline, setIsOnline] = useState(true);
// Checks to see if the browser has internet connection or not
window.addEventListener("online", () => setIsOnline(true));
window.addEventListener("offline", () => setIsOnline(false));
//Link to the image
const idleImgUrl = `${window.location.href}${coffeeMachine}`;
//convert image to base64 and save to local storage
imageToBase64(idleImgUrl)
.then(res => {
window.localStorage.setItem("idleImgData", res);
})
.catch(err => {
console.log(err);
});
return (
isOnline
? (<div>The web page to show</div>)
:
// <p> tag shows
<p>The browser is offline now</p>
// img tag does not show
(<img src={window.localStorage.getItem("idleImgData"} />)
);
};
Any help would be appreciated...
The trick is to load the image while the user agent still has an internet connection. The image won't be downloaded until you render the <img> tag. The cached image can then be displayed without issue later.
I wrote a short create-react-app example to illustrate.
import React, { useState, useEffect, useCallback } from 'react';
const App = () => {
const [online, setOnline] = useState(true);
const onlineListener = useCallback(() => setOnline(true), [setOnline]);
const offlineListener = useCallback(() => setOnline(false), [setOnline]);
useEffect(() => {
window.addEventListener('online', onlineListener);
window.addEventListener('offline', offlineListener);
return () => {
window.removeEventListener('online', onlineListener);
window.removeEventListener('offline', offlineListener);
};
}, [onlineListener, offlineListener]);
return (
<div className="App">
<img
style={online ? { display: 'none' } : undefined}
src="TODO"
alt="no internet"
/>
</div>
);
};
export default App;
It displays an image when the user agent loses connection and hides it again when connection is restored. It obviously won't work if connection is cut to begin with, but in such a case how did the user load your application 🤔?
If you are offline, you might not even be able to load your react bundle.js file in the first place and there is nothing you can do about it.
Also, I don't see the advantage of keeping it in your localStorage in this case. Browsers are probably going to cache it anyway, if size matters here.
If the user was able to load your bundle, you can just store the b64 hardcoded as a variable directly in your bundle or initiate an ajax on componentDidMount (using useEffect since you use hooks).
const Home = () => {
const [base64Img, setBase64Img] = useState();
const [isOnline, setIsOnline] = useState(true);
const setOnline = () => setIsOnline(true);
const setOffline = () => setIsOnline(false);
useEffect(() => {
initiateB64Retrieval().then((b64) => setBase64Img(b64));
window.addEventListener('online', setOnline);
window.addEventListener('offline', setOffline);
return () => {
window.removeEventListener('online', setOnline);
window.removeEventListener('offline', setOffline);
}
}, [])
...
}
Always good practice to remove your event listeners. Note that you cannot remove event listeners if passed with anonymous functions or when using .bind() as it creates another reference for the function.
So I have this method:
useEffect(() => {
//.. other logic here
// Firefox doesn't support looping video, so we emulate it this way
video.addEventListener(
"ended",
function() {
video.play();
},
false
);
}, [videoWidth, videoHeight]);
Now it throws an error where it says:
Assignments to the 'interval' variable from inside React Hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property. Otherwise, you can move this variable directly inside useEffect.
I am confused on what does this mean? especially this part: To preserve the value over time, store it in a useRef Hook and keep the mutable value in the '.current' property.
The error is pointing you to the right direction. Use the useRef hook to reference the video element. Since the handleVideo function makes the dependencies of useEffect Hook change on every render we wrap the handleVideo definition into its own useCallback() Hook.
import React, { useEffect, useRef, useCallback } from "react";
function Video() {
const videoRef = useRef(null);
const handleVideo = useCallback(() => {
videoRef.current.play()
}, [])
useEffect(() => {
const video = videoRef.current
video.addEventListener('ended', handleVideo)
return () => {
video.removeEventListener('ended', handleVideo)
}
}, [handleVideo])
return <video width="400" controls ref={videoRef} src="https://www.w3schools.com/html/mov_bbb.mp4" />
}
try this way it will be work
const video = useRef(null);
const videoPlay = () => { //just hate stateFull Function
video.current.play();
}
useEffect(() => {
//.. other logic here
// Firefox doesn't support looping video, so we emulate it this way
video.addEventListener(
"ended",
videoPlay,
false
);
return video.removeEventListener("ended", videoPlay, true)
}, [videoWidth, videoHeight, video]);
<video ref={video}>
<source src={src} type="video/mp4" />
</video>
if put the code in jsfiddle or some whare like this we can help you more