javascriptyoutubeyoutube-livestreaming-api

After binding my stream to my broadcast using the Youtube live streaming API, my video stream does not appear on Youtube


Hey guys I have a live streaming video application and I want to use the youtube livestreaming api. I am able to successfully authenticate, create a broadcast, create a stream, and bind the stream to the broadcast. After that I am sending my video stream to my server which sends it to youtube using the ingestionAddress and the streamName that I get from the API. When I go to the new broadcast on my Youtube (screenshot below) I do not see the video that I am sending. I am new to the Youtube live streaming API so any hints or tips on what I am doing wrong or need to add would be appreciated.

I do not think it is a problem with my server as it sends a live stream to YouTube perfectly if I hardcode my Youtube Stream Key and it also sends video to Twitch perfectly. I think I am missing a step or doing something wrong on the client. Here is my javascript code that uses the api. The relevant parts how caps locks comments. I also included a screenshot of the UI buttons I am clicking below. Here is my server code.

import React, { useState, useEffect, useRef } from 'react'
import Navbar from '../../components/Navbar/Navbar'
import BroadcastButton from '../../components/Buttons/BroadcastButton'
import Timer from '../../components/Timer/Timer'
import formatTime from '../../utils/formatTime'
import getCookie from '../../utils/getCookie'
import API from '../../api/api'
import './Broadcast.css'

const CAPTURE_OPTIONS = {
  audio: true,
  video: true,
}

/* global gapi */

function Broadcast() {
  const [isVideoOn, setisVideoOn] = useState(true)
  const [mute, setMute] = useState(false)
  const [seconds, setSeconds] = useState(0)
  const [isActive, setIsActive] = useState(false)

  const [youtubeIngestionUrl, setYoutubeIngestionUrl] = useState('')
  const [youtubeStreamName, setYoutubeStreamName] = useState('')
  const [facebookStreamKey, setFacebookStreamKey] = useState('')
  const [twitchStreamKey, setTwitchStreamKey] = useState('')

  const [mediaStream, setMediaStream] = useState(null)
  const [userFacing, setuserFacing] = useState(false)

  const [broadcastId, setbroadcastId] = useState('')

  const videoRef = useRef()
  const ws = useRef()

  const productionWsUrl = 'wss://www.ohmystream.xyz/websocket'
  const developmentWsUrl = 'ws://localhost:3001'

  //!!! THIS IS THE URL I AM STREAMING TO
  const youtubeUrl = youtubeIngestionUrl + '/' + youtubeStreamName

  const streamUrlParams = `?twitchStreamKey=${twitchStreamKey}&youtubeUrl=${youtubeUrl}&facebookStreamKey=${facebookStreamKey}`

  let liveStream
  let liveStreamRecorder

  if (mediaStream && videoRef.current && !videoRef.current.srcObject) {
    videoRef.current.srcObject = mediaStream
  }

  async function enableStream() {
    try {
      let stream = await navigator.mediaDevices.getUserMedia({
        video: isVideoOn,
        audio: true,
      })
      setMediaStream(stream)
    } catch (err) {
      console.log(err)
    }
  }

  useEffect(() => {
    if (!mediaStream) {
      enableStream()
    } else {
      return function cleanup() {
        mediaStream.getVideoTracks().forEach((track) => {
          track.stop()
        })
      }
    }
  }, [mediaStream])

  useEffect(() => {
    let userId = getCookie('userId')

    API.post('/destinations', { userId })
      .then((response) => {
        if (response) {
          setTwitchStreamKey(response.data.twitch_stream_key)
          setFacebookStreamKey(response.data.facebook_stream_key)
        }
      })
      .catch((err) => console.log(err))
  }, [])

  useEffect(() => {
    ws.current =
      process.env.NODE_ENV === 'production'
        ? new WebSocket(productionWsUrl + streamUrlParams)
        : new WebSocket(developmentWsUrl + streamUrlParams)

    console.log(ws.current)

    ws.current.onopen = () => {
      console.log('WebSocket Open')
    }

    return () => {
      ws.current.close()
    }
  }, [twitchStreamKey, youtubeStreamName])

  useEffect(() => {
    let interval = null
    if (isActive) {
      interval = setInterval(() => {
        setSeconds((seconds) => seconds + 1)
      }, 1000)
    } else if (!isActive && seconds !== 0) {
      clearInterval(interval)
    }
    return () => clearInterval(interval)
  }, [isActive, seconds])

  const toggle = () => {
    setIsActive(!isActive)
  }

  const startStream = () => {
    if (!twitchStreamKey || !youtubeStreamName) {
      alert(
        'Please add your twitch and youtube stream keys first under destinations'
      )
    } else {
      toggle()
      liveStream = videoRef.current.captureStream(30) // 30 FPS
      liveStreamRecorder = new MediaRecorder(liveStream, {
        mimeType: 'video/webm;codecs=h264',
        videoBitsPerSecond: 3 * 1024 * 1024,
      })
      liveStreamRecorder.ondataavailable = (e) => {
        ws.current.send(e.data)
        console.log('send data', e.data)
      }
      // Start recording, and dump data every second
      liveStreamRecorder.start(1000)
    }
  }

  const stopStream = () => {
    setIsActive(false)
    ws.current.close()
    liveStreamRecorder = null
    // liveStreamRecorder.stop()
  }

  const toggleMute = () => {
    setMute(!mute)
  }

  const toggleCamera = () => {
    // toggle camera on and off here
    setisVideoOn(false)
  }

  const recordScreen = async () => {
    let stream
    !userFacing
      ? (stream = await navigator.mediaDevices.getDisplayMedia(CAPTURE_OPTIONS))
      : (stream = await navigator.mediaDevices.getUserMedia(CAPTURE_OPTIONS))
    setMediaStream(stream)

    videoRef.current.srcObject = stream
    setuserFacing(!userFacing)
  }

  const handleCanPlay = () => {
    videoRef.current.play()
  }

  //!!! authenticate AND loadClient ARE CALLED FIRST
  const authenticate = () => {
    return gapi.auth2
      .getAuthInstance()
      .signIn({ scope: 'https://www.googleapis.com/auth/youtube.force-ssl' })
      .then((res) => {
        console.log(res)
      })
      .catch((err) => console.log(err))
  }

  const loadClient = () => {
    gapi.client.setApiKey(process.env.REACT_APP_GOOGLE_API_KEY)
    return gapi.client
      .load('https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest')
      .then((res) => {
        console.log('GAPI client loaded for API')
        console.log(res)
      })
      .catch((err) => console.log('Error loading GAPI client for API', err))
  }

  //!!! createBroadcast IS CALLED SECOND. BROADCAST APPEARS ON YOUTUBE
  const createBroadcast = () => {
    return gapi.client.youtube.liveBroadcasts
      .insert({
        part: ['id,snippet,contentDetails,status'],
        resource: {
          snippet: {
            title: `New Video: ${new Date().toISOString()}`,
            scheduledStartTime: `${new Date().toISOString()}`,
            description:
              'A description of your video stream. This field is optional.',
          },
          contentDetails: {
            recordFromStart: true,
            // startWithSlate: true,
            enableAutoStart: false,
            monitorStream: {
              enableMonitorStream: false,
            },
          },
          status: {
            privacyStatus: 'public',
            selfDeclaredMadeForKids: true,
          },
        },
      })
      .then((res) => {
        console.log('Response', res)
        console.log(res.result.id)
        setbroadcastId(res.result.id)
      })
      .catch((err) => {
        console.error('Execute error', err)
      })
  }

  //!!! CALL createStream AFTER createBroadcast. IN THE RESPONSE SET youtubeIngestionUrl AND youtubeStreamName
  const createStream = () => {
    return gapi.client.youtube.liveStreams
      .insert({
        part: ['snippet,cdn,contentDetails,status'],
        resource: {
          snippet: {
            title: "Your new video stream's name",
            description:
              'A description of your video stream. This field is optional.',
          },
          cdn: {
            frameRate: 'variable',
            ingestionType: 'rtmp',
            resolution: 'variable',
            format: '',
          },
          contentDetails: {
            isReusable: true,
          },
        },
      })
      .then((res) => {
        console.log('Response', res)

        setYoutubeIngestionUrl(res.result.cdn.ingestionInfo.ingestionAddress)
        console.log(res.result.cdn.ingestionInfo.ingestionAddress)

        setYoutubeStreamName(res.result.cdn.ingestionInfo.streamName)
        console.log(res.result.cdn.ingestionInfo.streamName)
      })
      .catch((err) => {
        console.log('Execute error', err)
      })
  }

  //!!! LAST FUNCTION TO BE CALLED BEFORE GOING LIVE.
  const bindBroadcastToStream = () => {
    return gapi.client.youtube.liveBroadcasts
      .bind({
        part: ['id,snippet,contentDetails,status'],
        id: broadcastId,
      })
      .then((res) => {
        console.log('Response', res)
      })
      .catch((err) => {
        console.error('Execute error', err)
      })
  }

  gapi.load('client:auth2', function () {
    gapi.auth2.init({
      client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
    })
  })

  return (
    <>
      <Navbar />
      <div className='dashboard-container'>
        <div id='container'>
          <div
            style={
              seconds === 0
                ? { visibility: 'hidden' }
                : { visibility: 'visible' }
            }
          >
            <Timer>
              {isActive ? 'LIVE' : 'END'}: {formatTime(seconds)}
            </Timer>
          </div>
          <video
            className='video-container'
            ref={videoRef}
            onCanPlay={handleCanPlay}
            autoPlay
            playsInline
            muted={mute}
          />
        </div>
        <div className='button-container'>
          <BroadcastButton
            title={!isActive ? 'Go Live' : 'Stop Recording'}
            fx={!isActive ? startStream : stopStream}
          />
          {/* <BroadcastButton title='Disable Camera' fx={toggleCamera} /> */}
          <BroadcastButton
            title={!userFacing ? 'Share Screen' : 'Stop Sharing'}
            fx={recordScreen}
          />
          <BroadcastButton title={!mute ? 'Mute' : 'Muted'} fx={toggleMute} />
        </div>

        <div style={{ marginTop: '1rem' }}>
          <button onClick={() => authenticate().then(loadClient)}>
            1. authenticate
          </button>
          <button onClick={createBroadcast}>2. create broadcast</button>
          <button onClick={createStream}>3. create stream</button>
          <button onClick={bindBroadcastToStream}>4. bind broadcast</button>
        </div>
      </div>
    </>
  )
}

export default Broadcast

Newly Created Youtube Broadcast enter image description here


Solution

  • Ok so I got a working solution by adding two things: 1) bind my streamID to my bindBroadcastToStream function 2) create a transitionToLive function which transitions the live broadcast broadcastStatus to live. Note you need to GO LIVE (i.e. click the button that says GO LIVE), before transitioning broadcastStatus to live. Here's the updated code below:

    import React, { useState, useEffect, useRef } from 'react'
    import Navbar from '../../components/Navbar/Navbar'
    import BroadcastButton from '../../components/Buttons/BroadcastButton'
    import Timer from '../../components/Timer/Timer'
    import formatTime from '../../utils/formatTime'
    import getCookie from '../../utils/getCookie'
    import API from '../../api/api'
    import './Broadcast.css'
    
    const CAPTURE_OPTIONS = {
      audio: true,
      video: true,
    }
    
    /* global gapi */
    
    function Broadcast() {
      const [isVideoOn, setisVideoOn] = useState(true)
      const [mute, setMute] = useState(false)
      const [seconds, setSeconds] = useState(0)
      const [isActive, setIsActive] = useState(false)
    
      const [youtubeIngestionUrl, setYoutubeIngestionUrl] = useState('')
      const [youtubeStreamName, setYoutubeStreamName] = useState('')
      const [facebookStreamKey, setFacebookStreamKey] = useState('')
      const [twitchStreamKey, setTwitchStreamKey] = useState('')
    
      const [mediaStream, setMediaStream] = useState(null)
      const [userFacing, setuserFacing] = useState(false)
    
      const [streamId, setstreamId] = useState('')
      const [broadcastId, setbroadcastId] = useState('')
    
      const videoRef = useRef()
      const ws = useRef()
    
      const productionWsUrl = 'wss://www.ohmystream.xyz/websocket'
      const developmentWsUrl = 'ws://localhost:3001'
    
      //!!! THIS IS THE URL I AM STREAMING TO
      const youtubeUrl = youtubeIngestionUrl + '/' + youtubeStreamName
    
      const streamUrlParams = `?twitchStreamKey=${twitchStreamKey}&youtubeUrl=${youtubeUrl}&facebookStreamKey=${facebookStreamKey}`
    
      let liveStream
      let liveStreamRecorder
    
      if (mediaStream && videoRef.current && !videoRef.current.srcObject) {
        videoRef.current.srcObject = mediaStream
      }
    
      async function enableStream() {
        try {
          let stream = await navigator.mediaDevices.getUserMedia({
            video: isVideoOn,
            audio: true,
          })
          setMediaStream(stream)
        } catch (err) {
          console.log(err)
        }
      }
    
      useEffect(() => {
        if (!mediaStream) {
          enableStream()
        } else {
          return function cleanup() {
            mediaStream.getVideoTracks().forEach((track) => {
              track.stop()
            })
          }
        }
      }, [mediaStream])
    
      useEffect(() => {
        let userId = getCookie('userId')
    
        API.post('/destinations', { userId })
          .then((response) => {
            if (response) {
              setTwitchStreamKey(response.data.twitch_stream_key)
              setFacebookStreamKey(response.data.facebook_stream_key)
            }
          })
          .catch((err) => console.log(err))
      }, [])
    
      useEffect(() => {
        ws.current =
          process.env.NODE_ENV === 'production'
            ? new WebSocket(productionWsUrl + streamUrlParams)
            : new WebSocket(developmentWsUrl + streamUrlParams)
    
        console.log(ws.current)
    
        ws.current.onopen = () => {
          console.log('WebSocket Open')
        }
    
        return () => {
          ws.current.close()
        }
      }, [twitchStreamKey, youtubeStreamName])
    
      useEffect(() => {
        let interval = null
        if (isActive) {
          interval = setInterval(() => {
            setSeconds((seconds) => seconds + 1)
          }, 1000)
        } else if (!isActive && seconds !== 0) {
          clearInterval(interval)
        }
        return () => clearInterval(interval)
      }, [isActive, seconds])
    
      const toggle = () => {
        setIsActive(!isActive)
      }
    
      const startStream = () => {
        if (!twitchStreamKey || !youtubeStreamName) {
          alert(
            'Please add your twitch and youtube stream keys first under destinations'
          )
        } else {
          toggle()
          liveStream = videoRef.current.captureStream(30) // 30 FPS
          liveStreamRecorder = new MediaRecorder(liveStream, {
            mimeType: 'video/webm;codecs=h264',
            videoBitsPerSecond: 3 * 1024 * 1024,
          })
          liveStreamRecorder.ondataavailable = (e) => {
            ws.current.send(e.data)
            console.log('send data', e.data)
          }
          // Start recording, and dump data every second
          liveStreamRecorder.start(1000)
        }
      }
    
      const stopStream = () => {
        setIsActive(false)
        ws.current.close()
        liveStreamRecorder = null
        // liveStreamRecorder.stop()
      }
    
      const toggleMute = () => {
        setMute(!mute)
      }
    
      const toggleCamera = () => {
        // toggle camera on and off here
        setisVideoOn(false)
      }
    
      const recordScreen = async () => {
        let stream
        !userFacing
          ? (stream = await navigator.mediaDevices.getDisplayMedia(CAPTURE_OPTIONS))
          : (stream = await navigator.mediaDevices.getUserMedia(CAPTURE_OPTIONS))
        setMediaStream(stream)
    
        videoRef.current.srcObject = stream
        setuserFacing(!userFacing)
      }
    
      const handleCanPlay = () => {
        videoRef.current.play()
      }
    
      //!!! authenticate AND loadClient ARE CALLED FIRST
      const authenticate = () => {
        return gapi.auth2
          .getAuthInstance()
          .signIn({ scope: 'https://www.googleapis.com/auth/youtube.force-ssl' })
          .then((res) => {
            console.log(res)
          })
          .catch((err) => console.log(err))
      }
    
      const loadClient = () => {
        gapi.client.setApiKey(process.env.REACT_APP_GOOGLE_API_KEY)
        return gapi.client
          .load('https://www.googleapis.com/discovery/v1/apis/youtube/v3/rest')
          .then((res) => {
            console.log('GAPI client loaded for API')
            console.log(res)
          })
          .catch((err) => console.log('Error loading GAPI client for API', err))
      }
    
      //!!! createBroadcast IS CALLED SECOND. BROADCAST APPEARS ON YOUTUBE
      const createBroadcast = () => {
        return gapi.client.youtube.liveBroadcasts
          .insert({
            part: ['id,snippet,contentDetails,status'],
            resource: {
              snippet: {
                title: `New Video: ${new Date().toISOString()}`,
                scheduledStartTime: `${new Date().toISOString()}`,
                description:
                  'A description of your video stream. This field is optional.',
              },
              contentDetails: {
                recordFromStart: true,
                // startWithSlate: true,
                enableAutoStart: false,
                monitorStream: {
                  enableMonitorStream: false,
                },
              },
              status: {
                privacyStatus: 'public',
                selfDeclaredMadeForKids: true,
              },
            },
          })
          .then((res) => {
            console.log('Response', res)
            console.log(res.result.id)
            setbroadcastId(res.result.id)
          })
          .catch((err) => {
            console.error('Execute error', err)
          })
      }
    
      //!!! CALL createStream AFTER createBroadcast. IN THE RESPONSE SET youtubeIngestionUrl AND youtubeStreamName
      const createStream = () => {
        return gapi.client.youtube.liveStreams
          .insert({
            part: ['snippet,cdn,contentDetails,status'],
            resource: {
              snippet: {
                title: "Your new video stream's name",
                description:
                  'A description of your video stream. This field is optional.',
              },
              cdn: {
                frameRate: 'variable',
                ingestionType: 'rtmp',
                resolution: 'variable',
                format: '',
              },
              contentDetails: {
                isReusable: true,
              },
            },
          })
          .then((res) => {
            console.log('Response', res)
    
            setstreamId(res.result.id)
            console.log('streamID' + res.result.id)
    
            setYoutubeIngestionUrl(res.result.cdn.ingestionInfo.ingestionAddress)
            console.log(res.result.cdn.ingestionInfo.ingestionAddress)
    
            setYoutubeStreamName(res.result.cdn.ingestionInfo.streamName)
            console.log(res.result.cdn.ingestionInfo.streamName)
          })
          .catch((err) => {
            console.log('Execute error', err)
          })
      }
    
      //!!! LAST FUNCTION TO BE CALLED BEFORE GOING LIVE.
      const bindBroadcastToStream = () => {
        return gapi.client.youtube.liveBroadcasts
          .bind({
            part: ['id,snippet,contentDetails,status'],
            id: broadcastId,
            streamId: streamId,
          })
          .then((res) => {
            console.log('Response', res)
          })
          .catch((err) => {
            console.error('Execute error', err)
          })
      }
    
      const transitionToLive = () => {
        return gapi.client.youtube.liveBroadcasts
          .transition({
            part: ['id,snippet,contentDetails,status'],
            broadcastStatus: 'live',
            id: broadcastId,
          })
          .then((res) => {
            // Handle the results here (response.result has the parsed body).
            console.log('Response', res)
          })
          .catch((err) => {
            console.log('Execute error', err)
          })
      }
    
      gapi.load('client:auth2', function () {
        gapi.auth2.init({
          client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
        })
      })
    
      return (
        <>
          <Navbar />
          <div className='dashboard-container'>
            <div id='container'>
              <div
                style={
                  seconds === 0
                    ? { visibility: 'hidden' }
                    : { visibility: 'visible' }
                }
              >
                <Timer>
                  {isActive ? 'LIVE' : 'END'}: {formatTime(seconds)}
                </Timer>
              </div>
              <video
                className='video-container'
                ref={videoRef}
                onCanPlay={handleCanPlay}
                autoPlay
                playsInline
                muted={mute}
              />
            </div>
            <div className='button-container'>
              <BroadcastButton
                title={!isActive ? '5) Go Live' : 'Stop Recording'}
                fx={!isActive ? startStream : stopStream}
              />
              {/* <BroadcastButton title='Disable Camera' fx={toggleCamera} /> */}
              <BroadcastButton
                title={!userFacing ? 'Share Screen' : 'Stop Sharing'}
                fx={recordScreen}
              />
              <BroadcastButton title={!mute ? 'Mute' : 'Muted'} fx={toggleMute} />
            </div>
    
            <div style={{ marginTop: '1rem' }}>
              <button onClick={() => authenticate().then(loadClient)}>
                1. authenticate
              </button>
              <button onClick={createBroadcast}>2. create broadcast</button>
              <button onClick={createStream}>3. create stream</button>
              <button onClick={bindBroadcastToStream}>4. bind broadcast</button>
              <button onClick={transitionToLive}>6. transition to live</button>
            </div>
          </div>
        </>
      )
    }
    
    export default Broadcast