ImageFlux Live Streaming APIオプションを使って配信サイトを作ってみた

はじめに

近年、リモートワークの普及や実況配信等による配信プラットフォームの需要が高まっています。

今回は、バックエンド開発が不要で高品質なライブ配信環境を手軽に構築できる ImageFlux Live Streaming APIオプションの紹介と、実際に配信サイトを作ってみた記事になります。

ImageFlux Live Streaming と API オプションについて

ImageFlux Live Streaming はインフラ運用不要で、WebRTC SFUを使ったライブ配信プラットフォームを構築できるマネージドサービスです。

従来は、ライブ配信プラットフォームの開発に上記のサービスを利用した際、ユーザーインターフェイスなどのフロントエンド開発に加え、バックエンド開発も必要でした。

今回紹介する「APIオプション」は、ログイン認証、配信スケジュール管理、チャット等のライブ配信プラットフォームに欠かせない機能のAPIが利用でき、システム開発を素早く手軽に実装することが出来ます。

ニュースリリース:https://www.sakura.ad.jp/information/announcements/2021/11/30/1968208604/

curlで軽く触ってみる

APIオプションのドキュメントを参考に、curlコマンドを使って一部のAPIを叩いてみます。

APIドキュメント:https://opt.imageflux.jp/documents

※2022年8月現在、ImageFlux Live Streaming契約時にAPIオプションのトライアルが利用できます。

ログイン認証(APIトークン取得)

認証用APIに対して、アクセスコード・メールアドレス・パスワードで認証を行うことでAPIオプション用のトークンが発行されます。

$ curl --request POST \
  --url https://opt.imageflux.jp/api/login \
  --header 'Content-Type: application/json' \
  --data '{"email": "xxxxx", "password": "xxxxx", "access_code": "xxxxx"}'

{"token":"xxxxx","expires_at":"2022-07-27T14:43:42.000000Z"}

配信スケジュール登録・取得

配信スケジュールの登録や取得もcurlコマンドで実行できます。まずスケジュール登録はこちらです。

$ curl -i --request POST \
  --url https://opt.imageflux.jp/api/schedule \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer xxxxx' \
  --data '{"start_datetime":"2022/06/24 00:00:00","end_datetime":"2022/06/24 01:00:00","name":"API Test","imageflux_params":"[{\"durationSeconds\":1,\"startTimeOffset\":-2,\"video\":{\"width\":640,\"height\":480,\"fps\":12,\"bps\":2465792},\"audio\":{\"bps\":64000}}]"}'

{"ok":true,"shcedule_id":"c9e0681c-81e0-4fb8-aad2-8633d6f3248c"}

スケジュール取得の実行例はこちらです。

$ curl -i --request GET \
  --url https://opt.imageflux.jp/api/schedule \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer xxxxx'

[
  {
    "schedule_id": "c9e0681c-81e0-4fb8-aad2-8633d6f3248c",
    "name": "API Test",
    "start_datetime": "2022-06-23 15:00:00",
    "end_datetime": "2022-06-23 16:00:00",
    "created_datetime": "2022-06-23 04:43:21",
    "created_user_name": "深田宏興"
  }
]

サンプルページについて

ImageFlux Live Streaming契約時に発行されるアカウントを使って、APIオプションが提供しているサンプルページにログインして動作を確認できます。

サンプルページ:https://opt.imageflux.jp/sample/login

以下はサンプルページのスケジュール管理画面になります。

配信サイトを作ってみる

バックエンド開発はAPIオプションにより省略可能なため、フロントエンド開発のみでも配信サイトが作れます。

今回は、以下の構成で配信サイトを作ってみました。

  • Node.js 16.15.1
  • React 18.2.0

使用するnpmパッケージは以下のとおりです。

  • react-router 6.3.0
  • semantic-ui-react 2.1.3
  • ky 0.31.0
  • sora-js-sdk 2022.1.0
  • hls.js 1.1.5

Reactプロジェクト

今回使用するReactプロジェクトは、React公式が用意しているcreate-react-appというコマンドで生成されたシングルページアプリケーション用のテンプレートをベースにしています。

本記事で登場するファイル群の大まかな構成を以下に示します。src/App.jsx はテンプレートで生成されたファイルを直接編集、 src/components/ 配下は新たに作成したプログラムになります。

my-app
`-src
  `- App.jsx              ルーティング設定
  `- components
     `- organisms
         `- Login.jsx     ログイン認証画面
         `- Schedule.jsx  配信スケジュール画面
         `- View.jsx      視聴画面
         `- Webcast.jsx   配信画面

Node.jsをインストールした端末で、プロジェクトルートからnpm startまたはyarn startコマンドを実行することで開発用サーバが起動し、ウェブページの動作を確認することができます。

Reactのルーティング設定

APIオプションの配信や視聴・チャットAPIは配信スケジュール登録時に発行される一意のschedule_idを指定する必要があるため、配信画面(/webcast)と視聴画面(/view)のパスパラメータにschedule_idを持たせてAPIリクエスト時に使用します。

以下は App.jsx のサンプルコードです。

import { Navigate, Route, Routes } from "react-router";
import Login from "components/organisms/Login";
import Schedule from "components/organisms/Schedule";
import View from "components/organisms/View";
import Webcast from "components/organisms/Webcast";
import "./App.css";
import "semantic-ui-css/semantic.min.css";

const App = () => (
  <div className="App">
    <header className="App-header">
      <Routes>
        <Route path="/" element={<Login />} />
        <Route path="/schedule" element={<Schedule />} />
        <Route path="/view/:schedule_id" element={<View />} />
        <Route path="/webcast/:schedule_id" element={<Webcast />} />
        <Route path="*" element={<Navigate to="/" replace />} />
      </Routes>
    </header>
  </div>
);

export default App;

ログイン認証画面

ログイン認証APIにより認証が成功すると一定期間有効なAPIトークンが発行されるため、必要に応じてcookie等に保存してください。(今回はテスト目的のため、ログ出力されたトークン文字列を.envファイルに直接入力します)

以下は Login.jsx のサンプルコードです。

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, Form, Segment } from 'semantic-ui-react';
import ky from 'ky';

const Login = () => {
  const [alertText, setAlertText] = useState('');
  const [accessCode, setAccessCode] = useState('');
  const [passwordText, setPasswordText] = useState('');
  const [emailAddress, setEmailAddress] = useState('');
  const navigate = useNavigate();

  const handleSubmit = async (event) => {
    setAlertText('');
    event.preventDefault();
    try{
      const data = await ky.post('https://opt.imageflux.jp/api/login', {
        json: {
          email: emailAddress,
          password: passwordText,
          access_code: accessCode
        }
      }).json();
      console.log(data.token);
      // data.tokenをcookie等に保存する
      navigate('/schedule');
    } catch (error) {
      setAlertText(<Segment inverted color='red'>認証情報に誤りがあります</Segment>);
    }
  };

  return (
    <Card>
      <Card.Content>
        <Form onSubmit={handleSubmit}>
          <Form.Input label='アクセスコード' placeholder='Access Code' onChange={(e) => {setAccessCode(e.target.value)}}/>
          <Form.Input label='メールアドレス' placeholder='Email Address' onChange={(e) => {setEmailAddress(e.target.value)}}/>
          <Form.Input label='パスワード' placeholder='Password' type='password' onChange={(e) => {setPasswordText(e.target.value)}}/>
          {alertText}
          <Form.Button type='submit'>ログイン</Form.Button>
        </Form>
      </Card.Content>
    </Card>
  )
};

export default Login;

配信スケジュール画面

配信スケジュールの一覧画面を作ります。

配信情報取得APIから得られるschedule_idを元に、配信画面と視聴画面のURLを生成しています。

以下は Schedule.jsx のサンプルコードです。

import React, { useState, useEffect } from 'react';
import { Container, Table, Button } from 'semantic-ui-react';
import ky from 'ky';
import Header from './Header';
import "./Schedule.css";

const token = process.env.REACT_APP_API_TOKEN;
const client = ky.create({
  prefixUrl: 'https://opt.imageflux.jp/api',
  headers: {
    'Authorization': `Bearer ${token}`
 }
});

const Schedule = () => {
  const [body, setBody] = useState('');

  useEffect(() => (
    async () => {
      try{
        const data = await client.get('schedule').json();

        const url = new URL(window.location.href);
        setBody(
          data.map((x) => (
            <Table.Row>
              <Table.Cell> {`${x.start_datetime}~\n${x.end_datetime}`}</Table.Cell>
              <Table.Cell> {x.name} </Table.Cell>
              <Table.Cell>
                <a href={`${url.origin}/view/${x.schedule_id}`}>
                  {`${url.origin}/view/${x.schedule_id}`}
                </a>
              </Table.Cell>
              <Table.Cell> {x.created_user_name} </Table.Cell>
              <Table.Cell>
                <Button primary href={`${url.origin}/webcast/${x.schedule_id}`}>配信</Button>
              </Table.Cell>
            </Table.Row>
          ))
        );
      } catch (error) {
        console.log(error);
      }
    }
  ), []);

  return (
    <Container>
      <Header schedule={true}/>
      <Table>
        <Table.Header>
          <Table.Row>
            <Table.HeaderCell width={3}>配信日程</Table.HeaderCell>
            <Table.HeaderCell width={3}>配信名</Table.HeaderCell>
            <Table.HeaderCell>視聴URL</Table.HeaderCell>
            <Table.HeaderCell width={2}>登録者名</Table.HeaderCell>
            <Table.HeaderCell width={2}> 操作 </Table.HeaderCell>
          </Table.Row>
        </Table.Header>
        {body}
      </Table>
    </Container>
  )
};

export default Schedule;

配信画面

配信者用の配信画面を作ります。

ImageFlux Live Streaming クイックスタートドキュメントのサンプルコードを参考に配信部分の実装を行いました。

また、サンプルと異なる点としてnpmパッケージのsora-js-sdkを利用しています。

チャット送信APIにテキストメッセージを送信するとAPIオプションのサーバにテキストデータが蓄積され、チャット取得APIのschedule_idをキーとしてチャット内容の一覧を取得することができます。

以下は View.jsx のサンプルコードです。

import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Container, Segment, Menu, List, Image, Form } from 'semantic-ui-react';
import ky from 'ky';
import Sora from 'sora-js-sdk';
import "./Webcast.css";

const token = process.env.REACT_APP_API_TOKEN;
const client = ky.create({
  prefixUrl: 'https://opt.imageflux.jp/api',
  headers: {
    'Authorization': `Bearer ${token}`
 }
});

const Webcast = () => {
  const { id } = useParams();
  const [body, setBody] = useState('');
  const [chatText, setChatText] = useState('');
  const [chatList, setChatList] = useState('');

  useEffect(() => (
    async () => {
      try{
        const data = await client.get(`delivery?schedule_id=${id}`).json();

        const {channel_id, sora_url} = data;
        const metadata = {};

        const debug = true;
        const sora = Sora.connection(sora_url, debug);
        const options = {
            multistream: false,
            audio: true,
            audioCodecType: 'OPUS',
            audioBitRate: 96,
            video: true,
            videoCodecType: 'H264',
            videoBitRate: 2000,
        }
        const publisher = sora.sendonly(channel_id, metadata, options);
        const constraints = {
            audio: true,
            video: {
                width: 1280,
                height: 720,
                frameRate: 30,
            }
        }
        navigator.mediaDevices.getUserMedia(constraints)
            .then(stream => publisher.connect(stream))
            .then(stream => {
                document.querySelector('#local-video').srcObject = stream;
            })
            .catch(e => { console.error(e); })
      } catch (error) {
        console.log(error);
      }
    }
  ), [id]);

  useEffect(() => {
    const intervalId = setInterval(async () => {
      try{
        const data = await client.get(`chat?schedule_id=${id}`).json();
        console.log(data);

        setChatList(
          data.map((x) => (
            <List.Item floated='left'>
              <Image avatar src='https://react.semantic-ui.com/images/avatar/small/rachel.png' />
              <List.Content>
                <List.Header>{x.name}</List.Header>
                <List.Description>{x.body}</List.Description>
              </List.Content>
              <List.Content floated='right'>
                <List.Description>{x.created_at}</List.Description>
              </List.Content>
            </List.Item>
          ))
        );
      } catch (error) {
        console.log(error);
      }
    }, 5000);

    return () => {
      clearInterval(intervalId)
    };
  }, [id]);

  const handleSubmit = async (event) => {
    event.preventDefault();
    try{
      const data = await client.post('chat', {
        json: {
          schedule_id: id,
          body: chatText,
        }
      }).json();
      setChatText('');
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <Container>
      <Menu>
        <Menu.Item>配信画面</Menu.Item>
      </Menu>
      <Container textAlign='center'>
        <video id="local-video" height="300" width="600" autoPlay controls muted/>
        {body}
      </Container>
      <Segment>
        <Segment style={{overflow: 'auto', maxHeight: 200 }}>
          <List divided relaxed floated='left'>
            {chatList}
          </List>
        </Segment>
        <Form onSubmit={handleSubmit}>
          <Form.Group>
          <Form.Input width={14} placeholder='text chat' value={chatText} onChange={(e) => {setChatText(e.target.value)}}/>
          <Form.Button width={2} type='submit'>送信</Form.Button>
          </Form.Group>
        </Form>
      </Segment>
    </Container>
  )
};

export default Webcast;

視聴画面

HLSで動画を受信している箇所を除いて、基本的には配信画面と同じ作りにしています。

配信画面と視聴画面を両方開くと、映像やチャットが同期して表示される事が確認できます。

以下は Webcast.jsx のサンプルコードです。

import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Container, Segment, Menu, List, Image, Form } from 'semantic-ui-react';
import ky from 'ky';
import Hls from 'hls.js';
import "./View.css";

const token = process.env.REACT_APP_API_TOKEN;
const client = ky.create({
  prefixUrl: 'https://opt.imageflux.jp/api',
  headers: {
    'Authorization': `Bearer ${token}`
 }
});

const View = () => {
  const { id } = useParams();
  const [body, setBody] = useState('');
  const [chatText, setChatText] = useState('');
  const [chatList, setChatList] = useState('');

  useEffect(() => (
    async () => {
      try{
        const data = await client.get(`view?schedule_id=${id}`).json();
        console.log(data);

        const {playlist_url} = data;
        const video = document.getElementById('video');
        if(Hls.isSupported()) {
          console.log(`hls.js version=${Hls.version}`);
          const hls = new Hls();
          hls.loadSource(playlist_url);
          hls.attachMedia(video);
        } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
          console.log("use native hls player");
          video.src = playlist_url;
          video.addEventListener('canplay',() => {
            video.play();
          });
        } else {
          console.log("sorry, can not play hls");
        }

      } catch (error) {
        console.log(error);
      }
    }
  ), [id]);

  ...

};

export default View;

おわりに

実際にAPIオプションを利用して作ってみた感想として、ログイン認証や各種管理機能が充実していてバックエンド開発が要らず、配信サイトを素早く実装する事ができました。また、開発工数を大幅に削減することが可能なため、少人数のスタートアップなどでも採用を検討してみて良さそうに感じました。

ImageFlux Live Streaming APIオプションに関するお問い合わせ先

メールアドレス: support@sakura.ad.jp