diff --git a/app/components-react/windows/go-live/platforms/YoutubeEditStreamInfo.tsx b/app/components-react/windows/go-live/platforms/YoutubeEditStreamInfo.tsx index 727db5444702..79b53d6ded13 100644 --- a/app/components-react/windows/go-live/platforms/YoutubeEditStreamInfo.tsx +++ b/app/components-react/windows/go-live/platforms/YoutubeEditStreamInfo.tsx @@ -6,7 +6,7 @@ import { InputComponent, ListInput, } from '../../../shared/inputs'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { Services } from '../../../service-provider'; import { $t } from '../../../../services/i18n'; import BroadcastInput from './BroadcastInput'; @@ -64,7 +64,11 @@ export const YoutubeEditStreamInfo = InputComponent((p: IPlatformComponentParams // re-fill form when the broadcastId selected useEffect(() => { - if (!broadcastId) return; + if (!broadcastId) { + // Cannot have monetization enabled before the broadcast is checked + updateSettings({ monetizationEnabled: false, eligibleForMonetization: false }); + return; + } Services.YoutubeService.actions.return .fetchStartStreamOptionsForBroadcast(broadcastId) .then(newYtSettings => { @@ -221,6 +225,9 @@ export const YoutubeEditStreamInfo = InputComponent((p: IPlatformComponentParams )}

)} + {!isScheduleMode && ytSettings.eligibleForMonetization && ( + + )} )} diff --git a/app/services/platforms/youtube.ts b/app/services/platforms/youtube.ts index 846939cac882..631db6c3dff1 100644 --- a/app/services/platforms/youtube.ts +++ b/app/services/platforms/youtube.ts @@ -57,6 +57,8 @@ export interface IYoutubeStartStreamOptions extends IExtraBroadcastSettings { privacyStatus?: 'private' | 'public' | 'unlisted'; scheduledStartTime?: number; mode?: TOutputOrientation; + monetizationEnabled?: boolean; + eligibleForMonetization?: boolean; } /** @@ -110,8 +112,24 @@ export interface IYoutubeLiveBroadcast { madeForKids: boolean; selfDeclaredMadeForKids: boolean; }; + monetizationDetails?: { + cuepointSchedule: { + enabled?: boolean; + pauseAdsUntil?: string; + creatorCuepointConfig?: any; + ytOptimizedCuepointConfig?: 'LOW' | 'MEDIUM' | 'HIGH'; + }; + adsMonetizationStatus?: 'on' | 'off'; + eligibleForAdsMonetization?: boolean; + }; } +type TYoutubeLiveBroadcastKey = keyof IYoutubeLiveBroadcast; +interface IYoutubeLiveBroadcastPatch + extends Partial< + Record> + > {} + /** * A liveStream resource contains information about the video stream that you are transmitting to YouTube. * The stream provides the content that will be broadcast to YouTube users. Once created, @@ -237,6 +255,7 @@ export class YoutubeService thumbnail: '', video: undefined, mode: undefined, + monetizationEnabled: false, display: 'horizontal', }, }; @@ -521,6 +540,11 @@ export class YoutubeService let broadcast: IYoutubeLiveBroadcast; if (!streamToScheduledBroadcast) { broadcast = await this.createBroadcast(ytSettings); + + // Current YT api doesn't let us POST with monetization settings so need to patch it in after creation + if (ytSettings.monetizationEnabled) { + await this.updateBroadcast(broadcast.id, ytSettings); + } } else { assertIsDefined(ytSettings.broadcastId); await this.updateBroadcast(ytSettings.broadcastId, ytSettings); @@ -862,7 +886,7 @@ export class YoutubeService const scheduledStartTime = params.scheduledStartTime ? new Date(params.scheduledStartTime) : new Date(); - const data: Dictionary = { + const data: IYoutubeLiveBroadcastPatch = { snippet: { title: params.title, scheduledStartTime: scheduledStartTime.toISOString(), @@ -932,7 +956,7 @@ export class YoutubeService scheduledStartTime: scheduledStartTime.toISOString(), }; - const contentDetails: Dictionary = { + const contentDetails: Partial = { enableAutoStart: isMidStreamMode ? broadcast.contentDetails.enableAutoStart : params.enableAutoStart, @@ -961,8 +985,36 @@ export class YoutubeService }; const fields = ['snippet', 'status', 'contentDetails']; + + let monetizationDetails: Partial; + if (broadcast.monetizationDetails) { + fields.push('monetizationDetails'); + this.usageStatisticsService.actions.recordFeatureUsage('YouTubeMonetization'); + + const moneyInfo = broadcast.monetizationDetails; + monetizationDetails = { + adsMonetizationStatus: isMidStreamMode + ? moneyInfo?.adsMonetizationStatus + : this.getMonetizationStatus(params.monetizationEnabled), + }; + if (!isMidStreamMode && params.monetizationEnabled) { + monetizationDetails.cuepointSchedule = { + ...moneyInfo.cuepointSchedule, + enabled: params.monetizationEnabled, + ytOptimizedCuepointConfig: 'MEDIUM', + creatorCuepointConfig: undefined, + }; + } + } + const endpoint = `liveBroadcasts?part=${fields.join(',')}&id=${id}`; - const body: Dictionary = { id, snippet, contentDetails, status }; + const body: IYoutubeLiveBroadcastPatch = { + id, + snippet, + contentDetails, + status, + monetizationDetails, + }; broadcast = await this.requestYoutube({ body: JSON.stringify(body), @@ -1110,7 +1162,7 @@ export class YoutubeService async fetchBroadcast( id: string, - fields = ['snippet', 'contentDetails', 'status'], + fields = ['snippet', 'contentDetails', 'status', 'monetizationDetails'], ): Promise { const filter = `&id=${id}`; const query = `part=${fields.join(',')}${filter}&maxResults=1`; @@ -1122,6 +1174,10 @@ export class YoutubeService ).items[0]; } + getMonetizationStatus(val: boolean) { + return val ? 'on' : 'off'; + } + get chatUrl() { const broadcastId = this.state.settings.broadcastId; if (!broadcastId) return ''; @@ -1140,6 +1196,10 @@ export class YoutubeService this.fetchBroadcast(broadcastId), this.fetchVideo(broadcastId), ]); + console.log('BROADCAST'); + console.log(JSON.stringify(broadcast, null, 2)); + console.log('VIDEO'); + console.log(JSON.stringify(video, null, 2)); const { title, description } = broadcast.snippet; const { privacyStatus, selfDeclaredMadeForKids } = broadcast.status; const { enableDvr, projection, latencyPreference } = broadcast.contentDetails; @@ -1154,6 +1214,8 @@ export class YoutubeService latencyPreference, categoryId: video.snippet.categoryId, thumbnail: broadcast.snippet.thumbnails.default.url, + monetizationEnabled: broadcast.monetizationDetails?.adsMonetizationStatus === 'on', + eligibleForMonetization: broadcast.monetizationDetails?.eligibleForAdsMonetization, }; }