Blog

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.

OAuth 2.0YouTube APIFacebook Live APINode.jsCatenoid

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

Request Account Link (1-time) Generate OAuth Auth URL Login/Consent Screen Consent Authorization Code Exchange for Access Token + Refresh Token Encrypt and Store Tokens Start Broadcast Create Live Broadcast (Auto-use Token) Stream Key Auto Configure Streaming Engine Complete Subsequent broadcasts are handled automatically User Backend YouTube

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:

  1. Log into YouTube Studio
  2. Sign in
  3. Click "Go Live"
  4. Copy the Stream Key and Server URL
  5. Return to the Catenoid Admin Console and paste them
  6. 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:

  1. access_type=offline is required: You must include this to receive a Refresh Token.
  2. prompt=consent setting: Forces the consent screen to appear even for previously authenticated accounts, ensuring you always receive a Refresh Token.
  3. Encrypt stored tokens: Never store them in plaintext in the DB. Encrypt them using AES-256 or similar.
  4. Refresh before expiration: Refreshing slightly early (e.g., 5 minutes before) rather than at the exact expiration time prevents race conditions.