Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -221,6 +225,9 @@ export const YoutubeEditStreamInfo = InputComponent((p: IPlatformComponentParams
)}
</p>
)}
{!isScheduleMode && ytSettings.eligibleForMonetization && (
<CheckboxInput label={$t('Enable Monetization')} {...bind.monetizationEnabled} />
)}
</>
)}
</InputWrapper>
Expand Down
70 changes: 66 additions & 4 deletions app/services/platforms/youtube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export interface IYoutubeStartStreamOptions extends IExtraBroadcastSettings {
privacyStatus?: 'private' | 'public' | 'unlisted';
scheduledStartTime?: number;
mode?: TOutputOrientation;
monetizationEnabled?: boolean;
eligibleForMonetization?: boolean;
}

/**
Expand Down Expand Up @@ -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<TYoutubeLiveBroadcastKey, Partial<IYoutubeLiveBroadcast[TYoutubeLiveBroadcastKey]>>
> {}

/**
* 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,
Expand Down Expand Up @@ -237,6 +255,7 @@ export class YoutubeService
thumbnail: '',
video: undefined,
mode: undefined,
monetizationEnabled: false,
display: 'horizontal',
},
};
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -862,7 +886,7 @@ export class YoutubeService
const scheduledStartTime = params.scheduledStartTime
? new Date(params.scheduledStartTime)
: new Date();
const data: Dictionary<any> = {
const data: IYoutubeLiveBroadcastPatch = {
snippet: {
title: params.title,
scheduledStartTime: scheduledStartTime.toISOString(),
Expand Down Expand Up @@ -932,7 +956,7 @@ export class YoutubeService
scheduledStartTime: scheduledStartTime.toISOString(),
};

const contentDetails: Dictionary<any> = {
const contentDetails: Partial<IYoutubeLiveBroadcast['contentDetails']> = {
enableAutoStart: isMidStreamMode
? broadcast.contentDetails.enableAutoStart
: params.enableAutoStart,
Expand Down Expand Up @@ -961,8 +985,36 @@ export class YoutubeService
};

const fields = ['snippet', 'status', 'contentDetails'];

let monetizationDetails: Partial<IYoutubeLiveBroadcast['monetizationDetails']>;
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<any> = { id, snippet, contentDetails, status };
const body: IYoutubeLiveBroadcastPatch = {
id,
snippet,
contentDetails,
status,
monetizationDetails,
};

broadcast = await this.requestYoutube<IYoutubeLiveBroadcast>({
body: JSON.stringify(body),
Expand Down Expand Up @@ -1110,7 +1162,7 @@ export class YoutubeService

async fetchBroadcast(
id: string,
fields = ['snippet', 'contentDetails', 'status'],
fields = ['snippet', 'contentDetails', 'status', 'monetizationDetails'],
): Promise<IYoutubeLiveBroadcast> {
const filter = `&id=${id}`;
const query = `part=${fields.join(',')}${filter}&maxResults=1`;
Expand All @@ -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 '';
Expand All @@ -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;
Expand All @@ -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,
};
}

Expand Down
Loading