Video displayed in ReactJS component not updating
Asked Answered
M

8

17

I'm new to ReactJS (0.13.1), and I've created a component in my app to display HTML5 video.

It seems to work perfectly but only for the first selection. The video that is actually displayed and playing in the page doesn't change when you switch from one video to another (when this.props.video changes).

I can see the <source src='blah.mp4' /> elements update in the Chrome inspector but the actually rendered video in the page doesn't change and keeps playing if it was already. Same thing happens in Safari & Firefox. All the other elements update appropriately as well.

Any ideas?

Anyway my component below:

(function(){
  var React = require('react');

  var VideoView = React.createClass({

    render: function(){
      var video = this.props.video;
      var title = video.title === ''? video.id : video.title;

      var sourceNodes = video.media.map(function(media){
        media = 'content/'+media;
        return ( <source src={media} /> )
      });
      var downloadNodes = video.media.map(function(media){
        var ext = media.split('.').slice(-1)[0].toUpperCase();
        media = 'content/'+media;
        return (<li><a className="greybutton" href={media}>{ext}</a></li>)
      });

      return (

        <div className="video-container">
          <video title={title} controls width="100%">
            {sourceNodes}
          </video>
          <h3 className="video-title">{title}</h3>
          <p>{video.description}</p>
            <div className="linkbox">
              <span>Downloads:</span>
              <ul className="downloadlinks">
                {downloadNodes}
              </ul>    
            </div>
        </div>

      )
    }
  });
  module.exports = VideoView;
})();

UPDATE:

To describe it another way:

I have a list of links with onClick handlers that set the props of the component.

When I click on a video link ("Video Foo") for the first time I get

<video title="Video Foo" controls>
  <source src="video-foo.mp4"/>
  <source src="video-foo.ogv"/>
</video>

and "Video Foo" appears and can be played.

Then when I click on the next one ("Video Bar") the DOM updates and I get

<video title="Video Bar" controls>
  <source src="video-bar.mp4"/>
  <source src="video-bar.ogv"/>
</video>

However it is still "Video Foo" that is visible and can be played.

It's like once the browser has loaded media for a <video> it ignores any changes to the <source> elements.

Moya answered 27/3, 2015 at 1:12 Comment(1)
type is optional, the browser queries the server providing the file for a type if it isn't provided.Moya
M
6

Found the answer

Dynamically modifying a source element and its attribute when the element is already inserted in a video or audio element will have no effect. To change what is playing, just use the src attribute on the media element directly, possibly making use of the canPlayType() method to pick from amongst available resources. Generally, manipulating source elements manually after the document has been parsed is an unnecessarily complicated approach

https://html.spec.whatwg.org/multipage/embedded-content.html#the-source-element

It's a pretty hacky and fragile, but it got the job done for my cases.

(function(){
  var React = require('react');

  var VideoView = React.createClass({

    pickSource: function(media){
      var vid = document.createElement('video');

      var maybes = media.filter(function(media){
        var ext = media.split('.').slice(-1)[0].toUpperCase();
        return (vid.canPlayType('video/'+ext) === 'maybe');
      });

      var probablies = media.filter(function(media){
        var ext = media.split('.').slice(-1)[0].toUpperCase();
        return (vid.canPlayType('video/'+ext) === 'probably');
      });

      var source = '';

      if (maybes.length > 0) { source = maybes[0]; }
      if (probablies.length > 0) { source = probablies[0]; }
      source = (source === '')? '' : 'content/'+source;
      return source;
    },

    render: function(){
      var video = this.props.video;
      var title = video.title === ''? video.id : video.title;

      var src = this.pickSource(video.media);

      var downloadNodes = video.media.map(function(media){
        var ext = media.split('.').slice(-1)[0].toUpperCase();
        media = 'content/'+media;
        return (
          <li><a className="greybutton" href={media}>{ext}</a></li>
        )
      });

      return (

        <div className="video-container">   
          <video title={title} src={src} controls width="100%"></video>
          <h3 className="video-title">{title}</h3>
          <p>{video.description}</p>
            <div className="linkbox">
              <span>Downloads:</span>
              <ul className="downloadlinks">
                {downloadNodes}
              </ul>
            </div>

        </div>

      )
    }
  });

  module.exports = VideoView;

})();
Moya answered 27/3, 2015 at 5:8 Comment(0)
P
21

I have described some approaches for plain JavaScript here. Based on that I have found solutions for React which work for me:

  • using src attribute on video itself:

    var Video = React.createComponent({
      render() {
        return <video src={this.props.videoUrl} />;
      }
    });
    

    Dana's answer is a great option extending this solution.

  • using .load() call on video element:

    var Video = React.createComponent({
      componentDidUpdate(_prevProps, _prevState) {
        React.findDOMNode(this.refs.video).load(); // you can add logic to check if sources have been changed
      },
    
      render() {
        return (
          <video ref="video">
            {this.props.sources.map(function (srcUrl, index) {
              return <source key={index} src={srcUrl} />;
            })}
          </video>
        );
      }
    });
    

UPD:

  • of course it's possible to add unique key attribute for <video> tag (for example based on your sources), so when sources will change it will be changed as well. But it will cause <video> to be re-rendered completely and it may cause some UI flashes.

    var Video = React.createComponent({
      render() {
        return (
          <video key={this.props.videoId}>
            {this.props.sources.map(function (srcUrl, index) {
              return <source key={index} src={srcUrl} />;
            })}
          </video>
        );
      }
    });
    
Pettit answered 25/8, 2015 at 22:51 Comment(0)
R
12

I faced the same issue and I didn't have access to the <video> HTML tag as I was using a library to render the video (not the native <video> HTML tag) which is internally responsible for rendering the <video> tag.

In this case I have found another solution which I think is better to solve the same issue.

Before:

<VideoLibrary src={this.props.src} />

After:

<React.Fragment key={this.props.src}>
  <VideoLibrary src={this.props.src} />
</React.Fragment>

Or this if you're using the native <video> HTML tag:

<React.Fragment key={this.props.src}>
  <video src={this.props.src} />
</React.Fragment>

This way React will render different video tags because the src prop will be different hence rendering a different HTML tag each time to avoid this issue.

I find this way cleaner and simpler and will work in both cases if you have or don't have access to the <video> HTML tag.

Randolf answered 7/1, 2019 at 8:23 Comment(1)
This worked for us!Normie
M
6

Found the answer

Dynamically modifying a source element and its attribute when the element is already inserted in a video or audio element will have no effect. To change what is playing, just use the src attribute on the media element directly, possibly making use of the canPlayType() method to pick from amongst available resources. Generally, manipulating source elements manually after the document has been parsed is an unnecessarily complicated approach

https://html.spec.whatwg.org/multipage/embedded-content.html#the-source-element

It's a pretty hacky and fragile, but it got the job done for my cases.

(function(){
  var React = require('react');

  var VideoView = React.createClass({

    pickSource: function(media){
      var vid = document.createElement('video');

      var maybes = media.filter(function(media){
        var ext = media.split('.').slice(-1)[0].toUpperCase();
        return (vid.canPlayType('video/'+ext) === 'maybe');
      });

      var probablies = media.filter(function(media){
        var ext = media.split('.').slice(-1)[0].toUpperCase();
        return (vid.canPlayType('video/'+ext) === 'probably');
      });

      var source = '';

      if (maybes.length > 0) { source = maybes[0]; }
      if (probablies.length > 0) { source = probablies[0]; }
      source = (source === '')? '' : 'content/'+source;
      return source;
    },

    render: function(){
      var video = this.props.video;
      var title = video.title === ''? video.id : video.title;

      var src = this.pickSource(video.media);

      var downloadNodes = video.media.map(function(media){
        var ext = media.split('.').slice(-1)[0].toUpperCase();
        media = 'content/'+media;
        return (
          <li><a className="greybutton" href={media}>{ext}</a></li>
        )
      });

      return (

        <div className="video-container">   
          <video title={title} src={src} controls width="100%"></video>
          <h3 className="video-title">{title}</h3>
          <p>{video.description}</p>
            <div className="linkbox">
              <span>Downloads:</span>
              <ul className="downloadlinks">
                {downloadNodes}
              </ul>
            </div>

        </div>

      )
    }
  });

  module.exports = VideoView;

})();
Moya answered 27/3, 2015 at 5:8 Comment(0)
N
1

Try this way

import React, { Component } from "react";

class Video extends Component<any, any> {

  video: any = React.createRef();

  componentDidUpdate(preProps: any) {
    const { url } = this.props;
    if (preProps && preProps.url && url) {
      if (preProps.url !== url) {
        this.video.current.src = url;
      }
    }
  }

  render() {
    const { url } = this.props;
    return (
      <video controls ref={this.video}>
        <source src={url} type="video/mp4" />
        Your browser does not support HTML5 video.
      </video>
    );
  }
}
Nim answered 10/2, 2020 at 11:56 Comment(0)
D
1

Thanks to this guy's blog, my similar issue was resolved just by adding a key attribute in the video tag.

<video key={displayItem.key} controls>
    <source src={`${Config.MEDIA_URL}${displayItem.awsfile}`} />
</video>

The accepted answer is probably outdated?

Doldrums answered 24/5, 2023 at 14:58 Comment(0)
C
0

I had the same problem with making a playlist with videos.

So I separated the video player to another react component and that component received two props: contentId (video identify) & videoUrl (video URL).

Also I added a ref to the video tag so I can manage the tag.

var Video = React.createClass({
    componentWillReceiveProps (nextProps) {
        if (nextProps.contentId != this.props.contentId) {
            this.refs['videoPlayer'].firstChild.src = this.props.videoUrl;
            this.refs['videoPlayer'].load()
        }
    },
    propType: {
        contentId: React.PropTypes.string, // this is the id of the video so you can see if they equal, if not render the component with the new url 
        videoUrl: React.PropTypes.string, // the video url
    } ,
    getDefaultProps(){
        return {
        };
    },
    render() {
        return (
            <video ref="videoPlayer" id="video" width="752" height="423">
                <source src={this.props.videoUrl} type="video/mp4" />
            </video>
        );
    }
});

module.exports = Video;

this is much more clean:

<Video contentId="12" videoUrl="VIDEO_URL" />
Cromlech answered 21/9, 2017 at 10:5 Comment(0)
S
0

Try to remove the source tag and instead have only the video tag and add src attribute to it like this example

<video src={video} className="lazy" loop autoPlay muted playsInline poster="" />
Saleme answered 19/2, 2021 at 15:0 Comment(0)
J
0

If you are getting the data from server and you want it to update the video link once you have new data.

    import React, {Fragment, useEffect, useState} from "react";
    const ChangeVID =(props)=> {
     
    const [prevUploaded, setPrevUploaded] =useState(null);
    useEffect(()=>{
    if(props.changeIMG) {
    setPrevUploaded(props.changeIMG)}

},[])
    return(<Fragment>
            {prevUploaded && <Suspense
                        fallback={<div>loading</div>}>
                        <div className="py-2" >
                            { <video id={prevUploaded.duration} width="320" height="240" controls >
                                        <source src={prevUploaded.url} type="video/mp4" />
                                    </video>}
        
                        </div>
                    </Suspense>
                    }<);
    }
Jacobi answered 15/10, 2021 at 18:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.