使用 Youtube 直播将我的流绑定到我的广播后 API,我的视频流没有出现在 Youtube 上
After binding my stream to my broadcast using the Youtube live streaming API, my video stream does not appear on Youtube
大家好,我有一个直播视频应用程序,我想使用 youtube livestreaming api。我能够成功地进行身份验证、创建广播、创建流并将流绑定到广播。之后,我将我的视频流发送到我的服务器,服务器使用我从 API 获得的 ingestionAddress 和 streamName 将它发送到 youtube。当我在我的 Youtube 上观看新广播时(下面的屏幕截图),我没有看到我正在发送的视频。我是 Youtube 直播的新手 API 所以任何关于我做错了什么或需要添加的提示或技巧将不胜感激。
我认为这不是我的服务器的问题,因为如果我对我的 Youtube 流密钥进行硬编码,它会完美地向 YouTube 发送实时流,并且它还会完美地向 Twitch 发送视频。我想我错过了一步或在客户身上做错了什么。这是我使用 api 的 javascript 代码。大写锁定评论的相关部分。我还在下面附上了我正在单击的 UI 按钮的屏幕截图。这是我的 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
好的,所以我通过添加两件事得到了一个可行的解决方案:1) 将我的 streamID 绑定到我的 bindBroadcastToStream 函数 2) 创建一个 transitionToLive 函数,它将直播 broadcastStatus 转换为直播。请注意,在将 broadcastStatus 转换为直播之前,您需要上线(即单击显示“上线”的按钮)。下面是更新后的代码:
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
大家好,我有一个直播视频应用程序,我想使用 youtube livestreaming api。我能够成功地进行身份验证、创建广播、创建流并将流绑定到广播。之后,我将我的视频流发送到我的服务器,服务器使用我从 API 获得的 ingestionAddress 和 streamName 将它发送到 youtube。当我在我的 Youtube 上观看新广播时(下面的屏幕截图),我没有看到我正在发送的视频。我是 Youtube 直播的新手 API 所以任何关于我做错了什么或需要添加的提示或技巧将不胜感激。
我认为这不是我的服务器的问题,因为如果我对我的 Youtube 流密钥进行硬编码,它会完美地向 YouTube 发送实时流,并且它还会完美地向 Twitch 发送视频。我想我错过了一步或在客户身上做错了什么。这是我使用 api 的 javascript 代码。大写锁定评论的相关部分。我还在下面附上了我正在单击的 UI 按钮的屏幕截图。这是我的 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
好的,所以我通过添加两件事得到了一个可行的解决方案:1) 将我的 streamID 绑定到我的 bindBroadcastToStream 函数 2) 创建一个 transitionToLive 函数,它将直播 broadcastStatus 转换为直播。请注意,在将 broadcastStatus 转换为直播之前,您需要上线(即单击显示“上线”的按钮)。下面是更新后的代码:
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