Simultaneous Broadcasting on YouTube & Facebook, Automating a 6-Step Setup into a Single Authentication via OAuth 2.0
Sharing the implementation process of reducing the 6-step manual setup for social media simulcasting on a live streaming platform into a single authentication flow using OAuth 2.0 automation.
Note: The code in this article has been conceptually rewritten based on actual work experience. It is not associated with the actual company code.
OAuth 2.0 Authentication + Auto Simulcast Flow
Introduction
Customers using Catenoid's live streaming solution had to go through the following process every single time they wanted to simulcast (syndicate) to YouTube or Facebook:
- Log into YouTube Studio
- Sign in
- Click "Go Live"
- Copy the Stream Key and Server URL
- Return to the Catenoid Admin Console and paste them
- Start Broadcasting
6 steps. And during this process, if they copied the stream key incorrectly or made a typo, it immediately turned into a customer service (CS) ticket. The vast majority of "Why isn't my simulcast working?" inquiries were due to stream key input errors.
This post details how we solved this problem through automation based on OAuth 2.0.
Design: Letting the Backend Handle Everything
The core approach was having the backend call the social platform APIs directly on behalf of the user.
[User] → [Link Account (1-time)] → [Save OAuth Tokens]
[User] → [Click Start Broadcast]
↓
[Backend Auto Processing]
1. Call YouTube API with saved tokens
2. Create Live Broadcast
3. Retrieve Stream Key
4. Auto-configure Catenoid Streaming Engine
5. Start Broadcast
Users only need to "Link YouTube Account" once, and all subsequent processes occur automatically.
Implementing the OAuth 2.0 Flow
1. Registering Apps per Platform
We registered apps on the respective developer consoles for YouTube and Facebook and requested the necessary scopes.
YouTube Required Scopes:
- https://www.googleapis.com/auth/youtube
- https://www.googleapis.com/auth/youtube.readonly
Facebook Required Scopes:
- publish_video
- pages_manage_posts
- pages_read_engagement
2. Implementing the Authorization Code Flow
// Generate Auth URL (Redirect user to YouTube Login page)
router.get('/auth/youtube/connect', (req, res) => {
const authUrl = `https://accounts.google.com/o/oauth2/auth?` +
`client_id=${YOUTUBE_CLIENT_ID}&` +
`redirect_uri=${encodeURIComponent(REDIRECT_URI)}&` +
`scope=${encodeURIComponent(YOUTUBE_SCOPES)}&` +
`response_type=code&` +
`access_type=offline&` // Mandatory for issuing Refresh Token
`prompt=consent`; // Force consent to always get Refresh Token
res.redirect(authUrl);
});
// Redirect URI Handler: Exchange Authorization Code for Tokens
router.get('/auth/youtube/callback', async (req, res) => {
const { code } = req.query;
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
body: new URLSearchParams({
code,
client_id: YOUTUBE_CLIENT_ID,
client_secret: YOUTUBE_CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
})
});
const { access_token, refresh_token, expires_in } = await tokenResponse.json();
// Encrypt tokens and save to DB
await saveTokens(req.user.id, 'youtube', {
accessToken: encrypt(access_token),
refreshToken: encrypt(refresh_token),
expiresAt: new Date(Date.now() + expires_in * 1000)
});
res.redirect('/settings/social?connected=youtube');
});
3. Automatically Refreshing the Access Token
Access Tokens typically expire after 1 hour. Logic to automatically refresh them using the Refresh Token is essential.
async function getValidAccessToken(userId: string, platform: string): Promise<string> {
const tokens = await db.socialTokens.findOne({ userId, platform });
// Refresh starting 5 minutes before expiration
if (tokens.expiresAt < new Date(Date.now() + 5 * 60 * 1000)) {
const refreshed = await refreshAccessToken(
platform,
decrypt(tokens.refreshToken)
);
await db.socialTokens.update({
where: { userId, platform },
data: {
accessToken: encrypt(refreshed.access_token),
expiresAt: new Date(Date.now() + refreshed.expires_in * 1000)
}
});
return refreshed.access_token;
}
return decrypt(tokens.accessToken);
}
Platform Abstraction: Making Adding New Platforms Easy
The APIs for YouTube and Facebook are completely different. We abstracted them into a common interface.
interface SocialStreamProvider {
createLiveBroadcast(title: string, scheduledAt: Date): Promise<string>;
getStreamKey(broadcastId: string): Promise<StreamKey>;
startBroadcast(broadcastId: string): Promise<void>;
endBroadcast(broadcastId: string): Promise<void>;
}
class YouTubeProvider implements SocialStreamProvider {
async createLiveBroadcast(title: string, scheduledAt: Date): Promise<string> {
const response = await youtube.liveBroadcasts.insert({
part: ['snippet', 'status', 'contentDetails'],
requestBody: {
snippet: { title, scheduledStartTime: scheduledAt.toISOString() },
status: { privacyStatus: 'public' },
contentDetails: { enableAutoStart: true }
}
});
return response.data.id;
}
// ...
}
class FacebookProvider implements SocialStreamProvider {
async createLiveBroadcast(title: string): Promise<string> {
const response = await fb.api(
`/${PAGE_ID}/live_videos`,
'POST',
{ title, status: 'LIVE_NOW' }
);
return response.id;
}
// ...
}
When adding a new platform (e.g., Twitch), we only need to implement this interface.
Automation Workflow
When the user clicks "Start Broadcast", this process runs automatically on the backend:
async function startSyndication(channelId: string, platforms: string[]) {
const results = await Promise.allSettled(
platforms.map(async (platform) => {
const provider = getProvider(platform); // YouTube | Facebook
const accessToken = await getValidAccessToken(channelId, platform);
// 1. Create Live Broadcast
const broadcastId = await provider.createLiveBroadcast(channel.title);
// 2. Retrieve Stream Key
const streamKey = await provider.getStreamKey(broadcastId);
// 3. Configure Catenoid Streaming Engine
await cateonoindStreamingEngine.addDestination({
channelId,
rtmpUrl: streamKey.ingestionAddress,
streamKey: streamKey.key
});
// 4. Start Broadcast
await provider.startBroadcast(broadcastId);
return { platform, broadcastId, status: 'STARTED' };
})
);
// Handle failed platforms
results.forEach(result => {
if (result.status === 'rejected') {
log.error('Syndication failed:', result.reason);
// Send Alert
}
});
}
By using Promise.allSettled, we ensured that even if one platform fails, broadcasting to the others continues.
Results
- Complete automation from a 6-step manual setup to a 1-time account linkage.
- Drastically reduced CS inquiries caused by mistyped stream keys.
- Achieved fully automated simultaneous broadcasting to YouTube and Facebook.
Caveats When Implementing OAuth
Some easily missed points during actual implementation:
access_type=offlineis required: You must include this to receive a Refresh Token.prompt=consentsetting: Forces the consent screen to appear even for previously authenticated accounts, ensuring you always receive a Refresh Token.- Encrypt stored tokens: Never store them in plaintext in the DB. Encrypt them using AES-256 or similar.
- Refresh before expiration: Refreshing slightly early (e.g., 5 minutes before) rather than at the exact expiration time prevents race conditions.