取り組みの背景 — なぜライブ配信機能が今の個人開発で武器になるのか
スマートフォンアプリにおけるライブ配信機能は、もはや大手プラットフォームだけの専売特許ではありません。2026年現在、フィットネス指導・語学レッスン・ゲーム実況・投資解説など、ニッチなコミュニティに特化したライブ配信アプリが個人開発者の手から次々と生まれ、着実に収益を上げています。
しかし「ライブ配信機能を実装したい」と思っても、従来のネイティブ開発では WebRTC の難解な設定、Agora SDK のネイティブバインディング、iOS / Android 両プラットフォームへの対応という高い壁がありましました。
Rork を使えば、この壁を大幅に低くできます。ここでは Rork × Agora SDK を組み合わせ、ライブ配信アプリを完全実装する手順を体系的に解説します。配信・視聴から、ギフト機能による収益化、多人数接続時の最適化まで、プロダクションレベルの実装を目指します。
この記事で学べること
- Agora SDK の Rork プロジェクトへの統合方法
- 配信者(Host)と視聴者(Audience)のロール設計
- リアルタイムチャット・ギフト機能の実装
- RevenueCat / Stripe を使ったギフト課金フロー
- 遅延・画質・スケーリングの最適化テクニック
対象読者
Rork でのアプリ開発経験があり、Supabase や Firebase など外部サービスの API 連携に慣れている方を想定しています。Agora の事前知識は不要です。
第1章 アーキテクチャ設計 — ライブ配信アプリの全体像
実装を始める前に、ライブ配信アプリの全体アーキテクチャを整理しておきましょう。
コンポーネント構成
ライブ配信アプリは大きく 4 つのレイヤーで構成されます。
クライアントレイヤー(Rork アプリ)
Rork で生成した React Native アプリが担当します。配信者画面・視聴者画面・チャット UI・ギフトアニメーションを実装します。
リアルタイム通信レイヤー(Agora)
Agora RTC SDK が映像・音声のリアルタイム伝送を担当します。エンジニアが WebRTC の細部を意識せずに使えるよう、高水準な API を提供しています。
バックエンドレイヤー(Supabase)
配信ルームの管理・チャットメッセージの永続化・ユーザー情報・ギフト履歴を Supabase(PostgreSQL + Realtime)で管理します。
課金レイヤー(RevenueCat / Stripe)
ギフトコインの購入・配信者への収益分配を RevenueCat のアプリ内課金または Stripe で実装します。
データフロー
[配信者] ─── Agora SDK ──→ [Agora Cloud] ──→ [視聴者1, 2, 3...]
↕ ↕
Supabase Realtime ─── チャット・ギフト ──→ Supabase
↕
RevenueCat ─── コイン購入 ─── App Store / Google Play
第2章 Agora プロジェクトのセットアップ
2-1. Agora アカウントと App ID の取得
- Agora Console にアクセスし、アカウントを作成します
- 「プロジェクトを作成」から新規プロジェクトを作成します
- Authentication Mode は「Secure mode (Token)」を選択します(本番環境では必須)
- 生成された App ID をメモしておきます
2-2. Agora Token Server の構築(Supabase Edge Functions)
セキュアモードでは、バックエンドで Agora Token を発行する必要があります。
// supabase/functions/agora-token/index.ts
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
import { RtcTokenBuilder, RtcRole } from "npm:agora-token@2.0.2";
const AGORA_APP_ID = Deno.env.get("AGORA_APP_ID")\!;
const AGORA_APP_CERTIFICATE = Deno.env.get("AGORA_APP_CERTIFICATE")\!;
serve(async (req) => {
if (req.method \!== "POST") return new Response("Method Not Allowed", { status: 405 });
const { channelName, uid, role } = await req.json();
const rtcRole = role === "host" ? RtcRole.PUBLISHER : RtcRole.SUBSCRIBER;
const expirationTimeInSeconds = 3600;
const privilegeExpiredTs = Math.floor(Date.now() / 1000) + expirationTimeInSeconds;
const token = RtcTokenBuilder.buildTokenWithUid(
AGORA_APP_ID, AGORA_APP_CERTIFICATE,
channelName, uid, rtcRole, privilegeExpiredTs
);
return new Response(JSON.stringify({ token, channelName, uid }), {
headers: { "Content-Type": "application/json" },
});
});デプロイ後、Supabase ダッシュボードで環境変数 AGORA_APP_ID と AGORA_APP_CERTIFICATE を設定します。
2-3. Supabase テーブルの作成
-- ライブ配信ルームテーブル
CREATE TABLE live_rooms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
host_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
channel_name TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
status TEXT DEFAULT 'scheduled', -- scheduled | live | ended
viewer_count INTEGER DEFAULT 0,
total_gifts INTEGER DEFAULT 0,
started_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- チャットメッセージ(Realtime 対応)
CREATE TABLE live_chat_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id UUID REFERENCES live_rooms(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id),
display_name TEXT NOT NULL,
content TEXT NOT NULL,
message_type TEXT DEFAULT 'chat', -- chat | gift | system
gift_type TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- コインバランス
CREATE TABLE coin_balances (
user_id UUID PRIMARY KEY REFERENCES auth.users(id),
balance INTEGER DEFAULT 0,
total_purchased INTEGER DEFAULT 0,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ギフト履歴
CREATE TABLE gift_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sender_id UUID REFERENCES auth.users(id),
receiver_id UUID REFERENCES auth.users(id),
room_id UUID REFERENCES live_rooms(id),
gift_type TEXT NOT NULL,
coin_cost INTEGER NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- RLS 設定
ALTER TABLE live_rooms ENABLE ROW LEVEL SECURITY;
ALTER TABLE live_chat_messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE coin_balances ENABLE ROW LEVEL SECURITY;
CREATE POLICY "誰でも閲覧可" ON live_rooms FOR SELECT USING (true);
CREATE POLICY "本人が作成可" ON live_rooms FOR INSERT WITH CHECK (auth.uid() = host_id);
CREATE POLICY "ホストが更新可" ON live_rooms FOR UPDATE USING (auth.uid() = host_id);
CREATE POLICY "誰でもチャット閲覧可" ON live_chat_messages FOR SELECT USING (true);
CREATE POLICY "認証済みが送信可" ON live_chat_messages FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "本人のみ残高閲覧可" ON coin_balances FOR SELECT USING (auth.uid() = user_id);第3章 Rork での実装 — 配信者画面
3-1. Agora Host フック
// hooks/useAgoraHost.ts
import { useState, useEffect, useRef } from "react";
import {
createAgoraRtcEngine, IRtcEngine,
ClientRoleType, ChannelProfileType, FrameRate,
} from "react-native-agora";
export function useAgoraHost({ channelName, token, uid }: {
channelName: string; token: string; uid: number;
}) {
const engineRef = useRef<IRtcEngine | null>(null);
const [isStreaming, setIsStreaming] = useState(false);
const [isMuted, setIsMuted] = useState(false);
const [isCameraOff, setIsCameraOff] = useState(false);
const [viewerCount, setViewerCount] = useState(0);
useEffect(() => { initEngine(); return cleanup; }, []);
const initEngine = async () => {
const engine = createAgoraRtcEngine();
engineRef.current = engine;
engine.initialize({
appId: process.env.EXPO_PUBLIC_AGORA_APP_ID\!,
channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
});
engine.setVideoEncoderConfiguration({
dimensions: { width: 1280, height: 720 },
frameRate: FrameRate.FrameRateFps30,
bitrate: 1500, // kbps
});
engine.setClientRole(ClientRoleType.ClientRoleBroadcaster);
engine.enableVideo();
engine.enableAudio();
engine.startPreview();
engine.addListener("onUserJoined", () => setViewerCount(prev => prev + 1));
engine.addListener("onUserOffline", () => setViewerCount(prev => Math.max(0, prev - 1)));
// Token 期限切れ時の自動更新
engine.addListener("onRequestToken", async () => {
const { data } = await supabase.functions.invoke("agora-token", {
body: { channelName, uid, role: "host" },
});
engine.renewToken(data.token);
});
};
const startStream = async () => {
await engineRef.current?.joinChannel(token, channelName, uid, {
clientRoleType: ClientRoleType.ClientRoleBroadcaster,
});
setIsStreaming(true);
};
const endStream = async () => {
await engineRef.current?.leaveChannel();
setIsStreaming(false);
};
const toggleMute = () => {
const newMuted = \!isMuted;
engineRef.current?.muteLocalAudioStream(newMuted);
setIsMuted(newMuted);
};
const toggleCamera = () => {
const newOff = \!isCameraOff;
engineRef.current?.enableLocalVideo(\!newOff);
setIsCameraOff(newOff);
};
const cleanup = () => {
engineRef.current?.stopPreview();
engineRef.current?.leaveChannel();
engineRef.current?.release();
};
return {
isStreaming, isMuted, isCameraOff, viewerCount,
startStream, endStream, toggleMute, toggleCamera,
switchCamera: () => engineRef.current?.switchCamera(),
};
}3-2. 配信者 UI
// screens/HostStreamScreen.tsx
import React from "react";
import { View, Text, TouchableOpacity, StyleSheet, Alert } from "react-native";
import { RtcSurfaceView, VideoSourceType } from "react-native-agora";
import { useAgoraHost } from "../hooks/useAgoraHost";
import { supabase } from "../lib/supabase";
import ChatOverlay from "../components/ChatOverlay";
import GiftAnimationOverlay from "../components/GiftAnimationOverlay";
export default function HostStreamScreen({ roomId, channelName, token, uid }: {
roomId: string; channelName: string; token: string; uid: number;
}) {
const host = useAgoraHost({ channelName, token, uid });
const handleStart = async () => {
await host.startStream();
await supabase.from("live_rooms")
.update({ status: "live", started_at: new Date().toISOString() })
.eq("id", roomId);
};
const handleEnd = () => Alert.alert("配信を終了しますか?", "視聴者が切断されます。", [
{ text: "キャンセル", style: "cancel" },
{ text: "終了する", style: "destructive", onPress: async () => {
await host.endStream();
await supabase.from("live_rooms")
.update({ status: "ended", ended_at: new Date().toISOString() })
.eq("id", roomId);
}},
]);
return (
<View style={styles.container}>
<RtcSurfaceView style={styles.camera}
canvas={{ uid: 0, sourceType: VideoSourceType.VideoSourceCamera }} />
<View style={styles.statusBar}>
<View style={styles.liveTag}><Text style={styles.liveText}>● LIVE</Text></View>
<Text style={styles.viewerText}>👁 {host.viewerCount}人が視聴中</Text>
</View>
<ChatOverlay roomId={roomId} />
<GiftAnimationOverlay roomId={roomId} />
<View style={styles.controls}>
<TouchableOpacity onPress={host.switchCamera} style={styles.controlBtn}>
<Text style={styles.btnIcon}>🔄</Text>
</TouchableOpacity>
<TouchableOpacity onPress={host.toggleCamera} style={styles.controlBtn}>
<Text style={styles.btnIcon}>{host.isCameraOff ? "📵" : "📹"}</Text>
</TouchableOpacity>
<TouchableOpacity onPress={host.toggleMute} style={styles.controlBtn}>
<Text style={styles.btnIcon}>{host.isMuted ? "🔇" : "🎤"}</Text>
</TouchableOpacity>
{host.isStreaming
? <TouchableOpacity onPress={handleEnd} style={styles.endBtn}>
<Text style={styles.endBtnText}>配信終了</Text>
</TouchableOpacity>
: <TouchableOpacity onPress={handleStart} style={styles.startBtn}>
<Text style={styles.startBtnText}>配信開始</Text>
</TouchableOpacity>
}
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#000" },
camera: { flex: 1 },
statusBar: { position: "absolute", top: 50, left: 16, right: 16, flexDirection: "row", alignItems: "center", gap: 12 },
liveTag: { backgroundColor: "#FF4444", paddingHorizontal: 12, paddingVertical: 4, borderRadius: 4 },
liveText: { color: "#fff", fontWeight: "bold", fontSize: 13 },
viewerText: { color: "#fff", fontSize: 14 },
controls: { position: "absolute", bottom: 40, left: 16, right: 16, flexDirection: "row", justifyContent: "space-around", alignItems: "center" },
controlBtn: { width: 52, height: 52, borderRadius: 26, backgroundColor: "rgba(255,255,255,0.2)", justifyContent: "center", alignItems: "center" },
btnIcon: { fontSize: 24 },
startBtn: { backgroundColor: "#FF4444", paddingHorizontal: 28, paddingVertical: 14, borderRadius: 8 },
startBtnText: { color: "#fff", fontWeight: "bold", fontSize: 16 },
endBtn: { backgroundColor: "#333", paddingHorizontal: 28, paddingVertical: 14, borderRadius: 8 },
endBtnText: { color: "#fff", fontWeight: "bold", fontSize: 16 },
});第4章 視聴者画面の実装
4-1. Agora Audience フック
// hooks/useAgoraAudience.ts
import { useState, useEffect, useRef } from "react";
import {
createAgoraRtcEngine, IRtcEngine,
ClientRoleType, ChannelProfileType,
} from "react-native-agora";
export function useAgoraAudience({ channelName, token, uid }: {
channelName: string; token: string; uid: number;
}) {
const engineRef = useRef<IRtcEngine | null>(null);
const [hostUid, setHostUid] = useState<number | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [networkQuality, setNetworkQuality] = useState(0);
useEffect(() => { join(); return () => { engineRef.current?.leaveChannel(); engineRef.current?.release(); }; }, []);
const join = async () => {
const engine = createAgoraRtcEngine();
engineRef.current = engine;
engine.initialize({
appId: process.env.EXPO_PUBLIC_AGORA_APP_ID\!,
channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting,
});
// 低遅延モード(Ultra Low Latency: < 1.5秒)
engine.setClientRole(ClientRoleType.ClientRoleAudience, { audienceLatencyLevel: 1 });
engine.enableVideo();
engine.addListener("onUserPublishStream", (connection, remoteUid) => {
setHostUid(remoteUid); setIsConnected(true);
});
engine.addListener("onUserUnpublishStream", () => {
setHostUid(null); setIsConnected(false);
});
engine.addListener("onNetworkQuality", (connection, uid, tx, rx) => setNetworkQuality(rx));
await engine.joinChannel(token, channelName, uid, {
clientRoleType: ClientRoleType.ClientRoleAudience,
});
};
return { hostUid, isConnected, networkQuality };
}4-2. 視聴者 UI(Agora 映像 + チャット + ギフトボタン)
視聴者画面では配信者の映像を RtcSurfaceView で表示し、画面下部に ChatOverlay と GiftPanel を重ねます。ギフトパネルにはギフト種別ごとのボタンを並べ、タップでコイン消費・アニメーション発火・Supabase へのギフトメッセージ投稿を行います。
第5章 ギフト機能と課金システムの実装
ライブ配信アプリの収益化において、ギフトシステムは最も効果的な手法のひとつです。
5-1. コイン購入(RevenueCat 連携)
// services/coinPurchase.ts
import Purchases from "react-native-purchases";
import { supabase } from "../lib/supabase";
export const COIN_PACKAGES = {
COINS_100: { productId: "coins_100", amount: 100, display: "¥120" },
COINS_500: { productId: "coins_500", amount: 500, display: "¥580" },
COINS_1200: { productId: "coins_1200", amount: 1200, display: "¥1,200" },
COINS_3000: { productId: "coins_3000", amount: 3000, display: "¥2,800" },
};
export async function purchaseCoins(productId: string, coinAmount: number) {
const offerings = await Purchases.getOfferings();
const pkg = offerings.current?.availablePackages.find(p => p.product.identifier === productId);
if (\!pkg) throw new Error("商品が見つかりません");
await Purchases.purchasePackage(pkg);
const { data: { user } } = await supabase.auth.getUser();
if (\!user) throw new Error("未認証ユーザー");
// アトミックにコイン残高を加算
await supabase.rpc("add_coins", { p_user_id: user.id, p_amount: coinAmount });
}-- コイン残高の加算(アトミック処理)
CREATE OR REPLACE FUNCTION add_coins(p_user_id UUID, p_amount INTEGER)
RETURNS INTEGER AS $$
DECLARE v_new_balance INTEGER;
BEGIN
INSERT INTO coin_balances (user_id, balance, total_purchased)
VALUES (p_user_id, p_amount, p_amount)
ON CONFLICT (user_id) DO UPDATE
SET balance = coin_balances.balance + p_amount,
total_purchased = coin_balances.total_purchased + p_amount,
updated_at = NOW()
RETURNING balance INTO v_new_balance;
RETURN v_new_balance;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;5-2. ギフト送信(コイン消費 + Realtime 通知)
// services/giftService.ts
import { supabase } from "../lib/supabase";
export const GIFTS = {
STAR: { type: "gift_small", coinCost: 10, emoji: "⭐", label: "スター" },
GEM: { type: "gift_medium", coinCost: 50, emoji: "💎", label: "ダイヤ" },
BURST: { type: "gift_large", coinCost: 200, emoji: "🎉", label: "バースト" },
} as const;
export async function sendGift(roomId: string, receiverId: string, giftKey: keyof typeof GIFTS) {
const gift = GIFTS[giftKey];
const { data: { user } } = await supabase.auth.getUser();
if (\!user) throw new Error("ログインが必要です");
// コイン確認・消費・履歴記録をアトミックに実行
const { data, error } = await supabase.rpc("send_gift", {
p_sender_id: user.id, p_receiver_id: receiverId,
p_room_id: roomId, p_gift_type: gift.type, p_coin_cost: gift.coinCost,
});
if (error) throw new Error(error.message);
// チャットにギフトメッセージを投稿 → Realtime で全視聴者に通知
await supabase.from("live_chat_messages").insert({
room_id: roomId, user_id: user.id, display_name: data.display_name,
content: `${gift.emoji} ${gift.label}を送りました!`,
message_type: "gift", gift_type: gift.type,
});
}-- ギフト送信のアトミック処理
CREATE OR REPLACE FUNCTION send_gift(
p_sender_id UUID, p_receiver_id UUID, p_room_id UUID,
p_gift_type TEXT, p_coin_cost INTEGER
) RETURNS JSON AS $$
DECLARE
v_balance INTEGER;
v_display_name TEXT;
BEGIN
SELECT balance INTO v_balance FROM coin_balances WHERE user_id = p_sender_id FOR UPDATE;
IF v_balance IS NULL OR v_balance < p_coin_cost THEN
RAISE EXCEPTION 'コインが不足しています';
END IF;
UPDATE coin_balances SET balance = balance - p_coin_cost, updated_at = NOW() WHERE user_id = p_sender_id;
INSERT INTO gift_transactions (sender_id, receiver_id, room_id, gift_type, coin_cost)
VALUES (p_sender_id, p_receiver_id, p_room_id, p_gift_type, p_coin_cost);
UPDATE live_rooms SET total_gifts = total_gifts + p_coin_cost WHERE id = p_room_id;
SELECT COALESCE(raw_user_meta_data->>'display_name', email) INTO v_display_name
FROM auth.users WHERE id = p_sender_id;
RETURN json_build_object('success', true, 'display_name', v_display_name);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;第6章 リアルタイムチャットの実装
// hooks/useLiveChat.ts
import { useState, useEffect } from "react";
import { supabase } from "../lib/supabase";
interface ChatMessage {
id: string; display_name: string; content: string;
message_type: "chat" | "gift" | "system"; gift_type?: string; created_at: string;
}
export function useLiveChat(roomId: string) {
const [messages, setMessages] = useState<ChatMessage[]>([]);
useEffect(() => {
// 過去100件を取得
supabase.from("live_chat_messages").select("*")
.eq("room_id", roomId).order("created_at", { ascending: true }).limit(100)
.then(({ data }) => { if (data) setMessages(data); });
// Realtime サブスクリプション
const channel = supabase.channel(`room:${roomId}`)
.on("postgres_changes", {
event: "INSERT", schema: "public",
table: "live_chat_messages", filter: `room_id=eq.${roomId}`,
}, ({ new: msg }) => {
setMessages(prev => [...prev, msg as ChatMessage].slice(-200));
})
.subscribe();
return () => { supabase.removeChannel(channel); };
}, [roomId]);
const sendMessage = async (content: string, displayName: string) => {
const { data: { user } } = await supabase.auth.getUser();
if (\!user) return;
await supabase.from("live_chat_messages").insert({
room_id: roomId, user_id: user.id,
display_name: displayName, content, message_type: "chat",
});
};
return { messages, sendMessage };
}第7章 パフォーマンス最適化とスケーリング
7-1. 配信遅延の最小化
Agora にはレイテンシモードが複数あります。
- 通常モード: 2〜4秒(安定性重視・大規模配信向け)
- 低遅延モード(Ultra Low Latency): 1.5秒以下(Q&Aなどインタラクション重視)
engine.setClientRole(ClientRoleType.ClientRoleAudience, {
audienceLatencyLevel: 1, // 低遅延モード
});7-2. ネットワーク品質に応じた画質の自動調整
engine.addListener("onNetworkQuality", (connection, uid, txQuality, rxQuality) => {
if (rxQuality >= 4) {
// 品質が悪い → 360p に降格
engine.setVideoEncoderConfiguration({
dimensions: { width: 640, height: 360 },
frameRate: FrameRate.FrameRateFps15, bitrate: 500,
});
} else if (rxQuality <= 2) {
// 品質が良い → 720p に戻す
engine.setVideoEncoderConfiguration({
dimensions: { width: 1280, height: 720 },
frameRate: FrameRate.FrameRateFps30, bitrate: 1500,
});
}
});7-3. 大規模配信(1,000人超)向け CDN ストリーミング
視聴者が大規模になる場合は Agora の CDN プッシュ機能を活用します。配信者が RTMP で CDN にプッシュし、視聴者は HLS で受信します。
await engine.startRtmpStreamWithTranscoding(
"rtmp://your-cdn/live/channel-name",
{
width: 1280, height: 720,
videoBitrate: 1500, videoFramerate: 30,
audioSampleRate: 44100, audioBitrate: 128, audioChannels: 2,
transcodingUsers: [{ uid: 0, x: 0, y: 0, width: 1280, height: 720, zOrder: 1, alpha: 1 }],
}
);第8章 よくあるエラーと対処法
Q1. Agora チャンネルへの接続が失敗する
原因と対処: Token の有効期限切れが最多原因です。onRequestToken イベントで自動更新を実装しておくと、1時間を超えた配信でも切断が発生しません(第3章のコード参照)。
Q2. iOS でカメラ映像が表示されない
原因と対処: Info.plist に NSCameraUsageDescription と NSMicrophoneUsageDescription が設定されているか確認してください。Rork のプロンプトで「カメラと音声の権限を追加」と指示すると自動設定されます。
Q3. Android で NDK エラーが発生してビルドが失敗する
原因と対処: react-native-agora は NDK が必要です。android/build.gradle の ndkVersion を "24.0.8215888" 以上に設定してください。
Q4. 配信の映像がカクカクする
原因と対処: サーマルスロットリング(デバイスの過熱による性能制限)が疑われます。配信中はビットレートを抑えてフレームレートを優先する設定が効果的です。視聴者は画質の低下よりコマ落ちに敏感です。
Q5. ギフト送信後にコイン残高がおかしい
原因と対処: send_gift RPC が SECURITY DEFINER で定義されているか確認してください。RLS をバイパスしてアトミックに処理する必要があります。関数定義から SECURITY DEFINER を外すと RLS が干渉して残高更新が失敗します。
第9章 収益化戦略 — ライブ配信アプリで稼ぐ設計
コイン→現金の換算モデル
一般的な収益分配の考え方です。
- プラットフォーム手数料: 30〜40%(App Store / Google Play の課金手数料を含む)
- 配信者への還元: 60〜70%
コインを現金に換算するレートを設計(例: 100コイン = 50円配信者収益)し、月次で Stripe Connect 等を使ってPayout を実施します。
スーパーリスナー月額サブスクリプション
ギフト課金に加え、月額サブスクリプション(「スーパーリスナー」)を設けると予測可能な収益が生まれます。
特典の例
- チャットでのバッジ表示
- コメントの優先表示
- 毎月の無料コイン支給(例: 100コイン/月)
詳しいサブスクリプション実装はRork × RevenueCat 課金完全実装ガイドをご参照ください。課金フローのセキュリティ設計についてはRork アプリ完全セキュリティ実装ガイドも合わせてご覧ください。
まとめ
ここではRork × Agora SDK を使ったライブ配信アプリの完全実装を解説しました。
- Agora プロジェクト設定と Supabase Edge Functions を使った Token 発行サーバー
- 配信者・視聴者それぞれの Agora RTC フックと UI の実装
- ギフト送信・コイン購入の課金フロー(RevenueCat × Supabase アトミック処理)
- Supabase Realtime を使ったリアルタイムチャット機能
- ネットワーク適応型画質制御と大規模配信時の CDN プッシュ
ライブ配信機能はニッチなコミュニティとの強い絆を生み出し、継続的な収益の柱となります。フィットネス・語学・投資などのジャンルで特化したライブ配信アプリを作り、個人開発の新たな収益源を開拓してみてください。
リアルタイム機能の基礎についてはRork でリアルタイムチャットアプリを構築する完全ガイドも参考になります。
ライブ配信やリアルタイム通信をさらに深く