Switch to next video from Youtube API (React) - javascript

I am attempting to have the next video in a channel play right after the other. Currently, the website has the videos showing one after the other, but my goal is to show one video and the second one plays right after the other is done. I have the function set up for the video ending, but right now it just causes an alert. I am using the Youtube Data API to pull in the videos and their information.
Here is a snippet of the code I am using:
constructor() {
super();
this.state = {
videos: [],
};
}
componentDidMount() {
fetch('https://www.googleapis.com/youtube/v3/search?key='APIKey'&channelId=UCXIJgqnII2ZOINSWNOGFThA&part=snippet,id&order=date&maxResults=2')
.then(results => {
return results.json();
}).then(data => {
let videos = data.items.map((videos) => {
return(
<div key={videos.items}>
<YouTube
className="player"
id="video"
videoId={videos.id.videoId}
opts={VIDEO_OPTS}
onEnd={this.playNextVideo}
/>
<h2>{videos.snippet.title}</h2>
<p className="channel">Video by: {videos.snippet.channelTitle}</p>
</div>
);
});
this.setState({videos: videos});
console.log("state", this.state.videos);
})
}
playNextVideo = () => {
alert('The video is done!');
}

I suggest you to do few things a little bit different.
First save the results.json(); to your videos variable in the state and not the whole youtube component, that's bad practice.
Second save another variable in your state that indicates the current playing video id (playingVideoId). Initialize it in the componentDidMount and change it in your playNextVideo function like this:
constructor() {
super();
this.index=0;
}
componentDidMount() {
fetch('https://www.googleapis.com/youtube/v3/search?key='APIKey'&channelId=UCXIJgqnII2ZOINSWNOGFThA&part=snippet,id&order=date&maxResults=2').then(results => {
this.setState({videos: results.json()});
this.setState({playingVideoId: this.state.videos[this.index]});
})}
playNextVideo = () => {
this.setState({playingVideoId: this.state.videos[++this.index]});
}
Now use the render function to render the component
render() {
return(
<YouTube
className="player"
id="video"
videoId={this.state.playingVideoId}
opts={VIDEO_OPTS}
onEnd={this.playNextVideo}
/>
);
}

Related

How to play one song from array at a time react js

I am building a simple music player but where I fail is at trying to execute one item from the array at a time. I am using React H5 Audio Player package to play the music. I am currently mapping through all the songs but I don't know how to properly play one at a time. I appreciate any help. I've been stuck on this for a few days.
import { SongContext } from '../../SongContext';
import AudioPlayer from 'react-h5-audio-player';
import 'react-h5-audio-player/lib/styles.css';
import './Player.css';
const Player = () => {
const { songs } = useContext(SongContext);
return (
<>
{songs.length > 0 &&
songs.map((song) => (
<div className="player" key={song.id}>
<AudioPlayer
// autoPlay
// src={song.preview}
showJumpControls={false}
customVolumeControls={[]}
customAdditionalControls={[]}
onPlay={() => console.log('playing')}
/>
</div>
))}
</>
);
};
export default Player;
Don't map all the songs at once, take a song by index (currentSong), and when it's done, use the onEnded event to increment the index, so the next one would play.
Example (codepen):
const Player = () => {
const { songs } = useContext(SongContext);
const [currentSong, setCurrentSong] = useState(0);
const song = songs[currentSong];
if(!song) return null; // don't render the player when no song is available
return (
<div className="player">
<AudioPlayer
autoPlay
src={song.preview}
showJumpControls={false}
customVolumeControls={[]}
customAdditionalControls={[]}
onPlay={() => console.log('playing')}
onEnded={() => setCurrentSong(i => i + 1)}
/>
</div>
);
};

Updating a React list without re-rendering said list

I'm trying to figure out how to render out a set of divs, without re-rendering the entire list as a new set is added.
So I've got a stateful component. Inside said stateful component, I've got a function that A, gets a list of post id's, and B, makes a request to each of those post id's and pushes the results to an array. Like so:
getArticles = () => {
axios.get(`${api}/topstories.json`)
.then(items => {
let articles = items.data;
let init = articles.slice(0,50);
init.forEach(item => {
axios.get(`${post}/${item}.json`)
.then(article => {
this.setState({ articles: [...this.state.articles, article.data]});
});
})
});
}
Then, I've got a second function that takes this information and outputs it to a list of posts. Like so:
mapArticles = () => {
let articles = this.state.articles.map((item, i) => {
let time = moment.unix(item.time).fromNow();
return(
<section className="article" key={i}>
<Link className="article--link" to={`/posts/${item.id}`}/>
<div className="article--score">
<FontAwesomeIcon icon="angle-up"/>
<p>{item.score}</p>
<FontAwesomeIcon icon="angle-down"/>
</div>
<div className="article--content">
<div className="article--title">
<h1>{item.title}</h1>
</div>
<div className="article--meta">
{item.by} posted {time}. {item.descendants ? `${item.descendants} comments.` : null}
</div>
</div>
<div className="article--external">
<a href={item.link} target="_blank">
<FontAwesomeIcon icon="external-link-alt"/>
</a>
</div>
</section>
)
});
return articles;
}
I then use {this.mapArticles()} inside the render function to return the appropriate information.
However, whenever the app loads in a new piece of data, it re-renders the entire list, causing a ton of jank. I.e., when the first request finishes, it renders the first div. When the second request finishes, it re-renders the first div and renders the second. When the third request finishes, it re-renders the first and second, and renders the third.
Is there a way to have React recognize that the div with that key already exists, and should be ignored when the state changes and the function runs again?
A technique that I use to only render the part that are new is to keep a cache map of already drawn obj, so in the render method I only render the new incoming elements.
Here is an example:
Take a look at https://codesandbox.io/s/wq2vq09pr7
In this code you can see that the List has an cache array and the render method
only draw new arrays
class RealTimeList extends React.Component {
constructor(props) {
super(props);
this.cache = [];
}
renderRow(message, key) {
return <div key={key}>Mesage:{key}</div>;
}
renderMessages = () => {
//let newMessages=this,props.newMessage
let newElement = this.renderRow(this.props.message, this.cache.length);
this.cache.push(newElement);
return [...this.cache];
};
render() {
return (
<div>
<div> Smart List</div>
<div className="listcontainer">{this.renderMessages()}</div>
</div>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = { message: "hi" };
}
start = () => {
if (this.interval) return;
this.interval = setInterval(this.generateMessage, 200);
};
stop = () => {
clearTimeout(this.interval);
this.interval = null;
};
generateMessage = () => {
var d = new Date();
var n = d.getMilliseconds();
this.setState({ title: n });
};
render() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button onClick={this.start}> Start</button>
<button onClick={this.stop}> Stop</button>
<RealTimeList message={this.state.message} />
</div>
);
}
}
If items arrive at the same time, wait till all items are fetched, then render:
getArticles = () => {
axios.get(`${api}/topstories.json`)
.then(items => {
let articles = items.data;
let init = articles.slice(0, 50);
Promise.all(init.map(item => axios.get(`${post}/${item}.json`)).then(articles => {
this.setState({
articles
});
})
});
}
If you really want to render immediately after an item is fetched, you can introduce a utility component that renders when promise resolves.
class RenderOnResolve extends React.Component {
state = null
componentDidMount() {
this.props.promise.then(data => this.setState(data))
}
render() {
return this.state && this.props.render(this.state);
}
}
// usage:
<RenderOnResolve promise={promise} render={this.articleRenderer}/>

With multiple audio players in React, pause all but currently playing player

I have a Playlist component that is importing n number of <AudioPlayer /> components dependent on how many tracks are in the playlist.
What I need to do is pause all other <AudioPlayer /> components except the one that is currently playing. Otherwise, you can play multiple players at once which is no bueno.
In my Playlist component, I'm able to grab both the currently playing player as well as the 'not playing' players: when a player is played, it fires the onPlay() handler which grabs the refs of all of the currently mounted players, then I compare that to the playlist and finally, extract the players that are currently not playing.
What I'm stuck in is updating the state of the players so that when I press play on a different player, all of the other players are paused.
The <AudioPlayer /> components have a prop of playing which I should be able to use to pass down the updated playing state when a new player is played.
How can I do this?
Here is my Playlist.js code:
import React, { Component } from 'react'
import AudioPlayer from 'react-cl-audio-player'
const streamUrl = 'https://mystreamurl.com/'
const waveUrl = 'https://mywaveformurl.com/waveforms/'
class Playlist extends Component {
state = {
playlist: this.props.tracks,
playing: false,
currentPlayer: '',
notPlaying: []
}
onPlay = (refName, index) => {
// Get list of AudioPlayer components from refs
const players = Object.keys(this.refs).map((key, i) => {
return this.refs[key]
})
if (players) {
// get AudioPlayer that is playing
const currentPlayer = players[index]
// get playlist items that are not playing
const notPlaying = this.state.playlist.filter(
(track, trackIndex) => {
return trackIndex !== index
}
)
// Compare playlist items that are not playing to AudioPlayer list
// to find AudioPlayer components that are not playing
var notPlayingPlayers = players.filter(function(obj) {
return notPlaying.some(function(obj2) {
return obj.props.alt == obj2.track_audio
})
})
console.log('Not Playing Players', notPlayingPlayers)
// ^^ this gets me the <AudioPlayer /> components that arent playing
const notPlayingPause = notPlayingPlayers.map((item, x) => {
this.setState({
playing: false // I know this isn't correct.
})
})
}
}
render() {
const tracks = this.props.tracks
const sku = this.props.sku
const price = this.props.price
const addToCart = this.props.addToCart
const trackList = tracks.map((track, index) => {
const songs = [
{
url: `${streamUrl}${track.track_audio}_128.mp3`,
cover: '',
artist: {
name: ' ',
song: track.track_title
}
}
]
return (
<li
key={sku + '-' + track.track_audio}
className="playlist-track"
>
<span className="playlist-name">{track.track_title}</span>
<AudioPlayer
key={track.track_audio}
index={index}
songs={songs}
autoplay={false}
src={`${waveUrl}${track.track_audio}.png`}
alt={track.track_audio}
ref={track.track_audio + index}
onPlay={this.onPlay.bind(this, 'player', index)}
playing={this.state.playing}
/>
<span className="playlist-price">$1.99</span>
<button
type="button"
className="button playlist-button"
onClick={() => {
addToCart({
product_id: track.track_product_id,
quantity: 1,
price: price,
cart_item_data: {
name: track.track_title
}
})
}}
>
Add To Cart
</button>
</li>
)
})
return (
<div className="playlist-wrap">
<ul className="playlist">{trackList}</ul>
</div>
)
}
}
export default Playlist
Here's an image if it helps to make it more clear:

Why a child component's state keeps clearing?

I have multiple layers of React components for getting an embed from a music service API, including a higher-order component that hits the API to populate the embed. My problem is that my lowest-level child component won't change state. I basically want the populated embed (lowest level component) to display an album cover, which disappears after clicking it (revealing an iframe), and whose state remains stable barring any change in props higher up (by the time this component is revealed, there should be no other state changes aside from focus higher up). Here's the code:
Parent:
return (
/*...*/
<Embed
embed={this.props.attributes.embed}
cb={updateEmbed}
/>
/*...*/
First child ( above):
render() {
const {embed, className, cb} = this.props;
const {error, errorType} = this.state;
const WithAPIEmbed = withAPI( Embed );
/*...*/
return <WithAPIEmbed
embed={embed[0]}
className={className}
cb={cb}
/>;
/*...*/
withAPI:
/*...*/
componentWillMount() {
this.setState( {fetching: true} );
}
componentDidMount() {
const {embed} = this.props;
if ( ! embed.loaded ) {
this.fetchData();
} else {
this.setState( {
fetching: false,
error: false,
} );
}
}
fetchData() {
/*... some API stuff, which calls the callback in the top level parent (cb()) setting the embed prop when the promise resolves -- this works just fine ...*/
}
render() {
const {embed, className} = this.props;
const {fetching, error, errorType} = this.state;
if ( fetching ) {
/* Return some spinner/placeholder stuff */
}
if ( error ) {
/* Return some error stuff */
}
return (
<WrappedComponent
{...this.props}
embed={embed}
/>
)
}
And finally the last child I'm interested in:
constructor() {
super( ...arguments );
this.state = {
showCover: true,
};
}
render() {
const {embed, setFocus, className} = this.props;
const {showCover} = this.state;
if ( showCover ) {
return [
<div key="cover-image" className={classnames( className )}>
<figure className='cover-art'>
<img src={embed.coverArt} alt={__( 'Embed cover image' )}/>
<i onClick={() => {
this.setState( {showCover: false,} );
}}>{icon}</i> // <-- Play icon referenced below.
</figure>
</div>,
]
}
return [
<div key="embed" className={className}>
<EmbedSandbox
html={iframeHtml}
type={embed.embedType}
onFocus={() => setFocus()}
/>
</div>,
];
}
My issue is that clicking the play icon should clear the album cover and reveal the iframe embed, but even though the click is registering, the state never changes (or does and then changes back). I believe it's because a higher-level component is mounting/unmounting and reinstantiating this component with its default state. I could move this state up the tree or use something like Flux, but I really feel I shouldn't need to do that, and that there's something fundamental I'm missing here.
The problem is that const WithAPIEmbed = withAPI( Embed ); is inside the render method. This creates a fresh WithAPIEmbed object on each render, which will be remounted, clearing any state below. Lifting it out of the class definition makes it stable and fixes the problem.

Using context.drawImage() with a MediaStream

I'm new to React and I am building an app that takes screen grabs from a MediaStream. Based on what I've seen, the best way to do that is to draw it onto a canvas element using the context.drawImage() method, passing in the HTMLVideoElement as an argument. Here's what my action creator looks like:
const RecordImage = function(video, canvas, encoder) {
if(!encoder) {
encoder = new GifReadWrite.Encoder()
encoder.setRepeat(0)
encoder.setDelay(100)
encoder.start()
}
const context = canvas.getContext('2d')
context.drawImage(video, 0, 0)
encoder.addFrame(context)
return {
type: RECORD_IMAGE,
payload: encoder
}
}
This worked in the past because the RecordImage action was being called from the same component that housed the <video /> and <canvas /> element, and I could pass them in like so:
takePic(event) {
event.preventDefault()
this.props.RecordImage(this.video, this.canvas, this.props.encoder)
}
...
render() {
return (
<div>
<video
ref = { video => this.video = video }
width = { this.props.constraints.video.width }
height = { this.props.constraints.video.height }
autoPlay = "true"
/>
<canvas
ref = { canvas => this.canvas = canvas }
width = { this.props.constraints.video.width }
height = { this.props.constraints.video.height }
/>
<button onClick = { this.takePic }>Take Picture</button>
</div>
)
}
However, I would like to house the "Take Picture" button in a different component. This is a problem because now I don't know how to access the <video /> and <canvas /> elements from a sibling component. Normally I would store the arguments I need as part of the state, but the drawImage() method needs to use the HTML elements themselves. I've heard it's a bad idea to store DOM elements in the state, so what would be the best way to go about this?
In React it is possible to directly call a function on one of your components.
You could add a 'getPicture' function in your video component which returns the video element?
Your video component:
getPicture() {
return this.video
}
...
render() {
return (
<div>
<video
ref = { video => this.video = video }
width = { this.props.constraints.video.width }
height = { this.props.constraints.video.height }
autoPlay = "true"
/>
</div>
)
}
The picture button component could look something like this:
takePic() {
const element = this.props.getPic
const encoder = new GifReadWrite.Encoder()
encoder.setRepeat(0)
encoder.setDelay(100)
encoder.start()
const canvas = document.createElement("canvas")
canvas.width = element.offsetWidth
canvas.height = element.offsetHeight
canvas.drawImage(video, 0, 0)
encoder.addFrame(context)
return {
type: RECORD_IMAGE,
payload: encoder
}
}
...
render() {
return (
<button onClick = { () => this.takePic() }>Take Picture</button>
)
}
And then a parent component to bind the button and the video component:
getPicture() {
return this.video.getPicture()
}
...
render() {
return (
<div>
<VideoElement ref="video => this.video = video"/>
<TakePictureButton getPic="() => this.getPicture()" />
</div>
)
}
Disclaimer: I'm not sure how the draw function works, but you get the idea
Many thanks to #stefan-van-de-vooren for setting me on the right path! My situation was complicated by the child components using Redux connect, so getting them to work required some additional setup.
First, set up the parent component to call one child from the other:
setMedia(pic) {
return this.control.getWrappedInstance().setMedia(pic)
}
getPicture() {
return this.display.getWrappedInstance().getPicture()
}
componentDidMount() {
this.setMedia( this.getPicture() )
}
render() {
return (
<div>
<DisplayContainer ref= { display => this.display = display } />
<ControlContainer ref= { control => this.control = control } />
</div>
)
}
Because Redux connect returns a higher order component, we need to use getWrappedInstance() to gain access to the child functions. Next, we need to enable getWrappedInstance() in our child components by telling connect to use refs. Also, we will set up our child functions.
DisplayContainer:
getPicture() {
return this.video
}
...
render() {
return ( <video ref= { video => this.video = video } /> )
}
export default connect(mapStateToProps, null, null, { withRef: true })(Display)
ControlContainer:
setMedia(pic) {
this.setState({ media: pic })
}
takePic(event) {
event.preventDefault()
this.props.RecordImage(this.state.media, this.canvas, this.props.encoder)
}
...
render() {
return(
<button onClick= { this.takePic }>Take Picture</button>
<canvas ref= { canvas => this.canvas = canvas } />
)
}
export default connect(mapStateToProps, mapDispatchToProps, null, { withRef: true })(Control)
The action creator remains the same:
const RecordImage = function(video, canvas, encoder) {
if(!encoder) {
encoder = new GifReadWrite.Encoder()
encoder.setRepeat(0)
encoder.setDelay(100)
encoder.start()
}
const context = canvas.getContext('2d')
context.drawImage(video, 0, 0)
encoder.addFrame(context)
return {
type: RECORD_IMAGE,
payload: encoder
}
}
I should note, we are using refs quite a bit here. It is necessary in this case because the context.drawImage() needs the actual <video /> element in order to work. However refs are sometimes considered an anti-pattern, and it is worth considering if there is a better approach. Read this article for more information: https://reactjs.org/docs/refs-and-the-dom.html#dont-overuse-refs
A better solution would be to grab the image directly from the MediaStream. There is an experimental grabFrame() method that does just that, but as of Feb 2018, there is limited browser support. https://prod.mdn.moz.works/en-US/docs/Web/API/ImageCapture/grabFrame

Categories