Zoom

コミュニケーションビデオ会議APISDKWebhookアプリ開発

コミュニケーションツール

Zoom

概要

Zoomは、ビデオ会議に特化したコミュニケーションプラットフォームです。最大1,000名の参加者をサポートし、画面共有、録画機能、ウェビナー機能を提供します。高品質な音声・映像品質を誇り、パンデミック後もリモートワークの定着により需要が継続しています。

詳細

Zoom(ズーム)は、2011年に設立されたビデオ会議サービスで、現在ではビデオ会議の代名詞的存在として世界中で利用されています。パンデミック後のリモートワーク定着により需要が継続し、教育、医療、エンタープライズ分野での採用が拡大しています。

Zoomの最大の特徴は、シンプルで使いやすいインターフェースと、安定した高品質なビデオ・音声通信です。Zoom SDKとZoom APIを活用することで、独自のアプリケーション内にZoom機能を統合したり、カスタムソリューションを構築できます。Webhookによるイベント通知、Apps for Zoomによる拡張機能開発、Zoom Phone APIによる電話統合など、豊富な開発者向け機能を提供しています。

2024年には、AI機能の強化、Zoom Apps Marketplaceの拡充、Zoom Contact Center API、新しいSDK機能など、開発者エコシステムがさらに充実しました。特に、Meeting SDK、Video SDK、Cloud Recording APIなどにより、Zoom機能を他のアプリケーションに組み込むことが容易になっています。

メリット・デメリット

メリット

  • 高品質通信: 安定した音声・ビデオ品質
  • 大規模対応: 最大1,000名の参加者をサポート
  • 使いやすさ: 直感的で分かりやすいユーザーインターフェース
  • 豊富なAPI: SDK、REST API、Webhook の充実
  • 録画・配信: クラウド録画、ライブストリーミング機能
  • 統合機能: カレンダー、CRM、LMS等との連携
  • セキュリティ: エンドツーエンド暗号化、待機室機能
  • 開発者エコシステム: Apps for Zoom、Marketplace

デメリット

  • コスト: 高機能プランは比較的高価
  • セキュリティ懸念: 過去のセキュリティ問題(現在は改善済み)
  • リソース消費: 高品質通信のためのCPU・帯域使用量
  • API制限: レート制限と利用制限
  • 依存性: Zoomサービスへの依存度の高さ
  • UI変更: 頻繁なUI更新による慣れの必要性

主要リンク

書き方の例

Zoom Meeting API の基本使用

const axios = require('axios');

class ZoomAPI {
  constructor(jwt) {
    this.jwt = jwt;
    this.baseURL = 'https://api.zoom.us/v2';
  }

  // 会議作成
  async createMeeting(userId, meetingOptions) {
    try {
      const response = await axios.post(
        `${this.baseURL}/users/${userId}/meetings`,
        {
          topic: meetingOptions.topic,
          type: 2, // スケジュール会議
          start_time: meetingOptions.start_time,
          duration: meetingOptions.duration,
          settings: {
            host_video: true,
            participant_video: true,
            waiting_room: true,
            mute_upon_entry: true,
            approval_type: 0 // 自動承認
          }
        },
        {
          headers: {
            'Authorization': `Bearer ${this.jwt}`,
            'Content-Type': 'application/json'
          }
        }
      );
      
      return response.data;
    } catch (error) {
      console.error('Error creating meeting:', error.response.data);
      throw error;
    }
  }

  // 会議一覧取得
  async listMeetings(userId) {
    try {
      const response = await axios.get(
        `${this.baseURL}/users/${userId}/meetings`,
        {
          headers: {
            'Authorization': `Bearer ${this.jwt}`
          }
        }
      );
      
      return response.data;
    } catch (error) {
      console.error('Error fetching meetings:', error.response.data);
      throw error;
    }
  }

  // 会議詳細取得
  async getMeeting(meetingId) {
    try {
      const response = await axios.get(
        `${this.baseURL}/meetings/${meetingId}`,
        {
          headers: {
            'Authorization': `Bearer ${this.jwt}`
          }
        }
      );
      
      return response.data;
    } catch (error) {
      console.error('Error fetching meeting details:', error.response.data);
      throw error;
    }
  }

  // 会議削除
  async deleteMeeting(meetingId) {
    try {
      await axios.delete(
        `${this.baseURL}/meetings/${meetingId}`,
        {
          headers: {
            'Authorization': `Bearer ${this.jwt}`
          }
        }
      );
      
      return { success: true };
    } catch (error) {
      console.error('Error deleting meeting:', error.response.data);
      throw error;
    }
  }
}

// 使用例
const zoomAPI = new ZoomAPI(process.env.ZOOM_JWT);

async function scheduleMeeting() {
  try {
    const meeting = await zoomAPI.createMeeting('[email protected]', {
      topic: 'Weekly Team Meeting',
      start_time: '2024-01-15T10:00:00Z',
      duration: 60
    });
    
    console.log('Meeting created:', meeting.join_url);
    return meeting;
  } catch (error) {
    console.error('Failed to create meeting:', error);
  }
}

Zoom SDK統合(Meeting SDK)

<!DOCTYPE html>
<html>
<head>
    <title>Zoom Meeting SDK Integration</title>
    <script src="https://source.zoom.us/2.16.0/lib/vendor/react.min.js"></script>
    <script src="https://source.zoom.us/2.16.0/lib/vendor/react-dom.min.js"></script>
    <script src="https://source.zoom.us/2.16.0/lib/vendor/redux.min.js"></script>
    <script src="https://source.zoom.us/2.16.0/lib/vendor/redux-thunk.min.js"></script>
    <script src="https://source.zoom.us/2.16.0/lib/vendor/lodash.min.js"></script>
    <script src="https://source.zoom.us/zoom-meeting-2.16.0.min.js"></script>
</head>
<body>
    <div id="meetingSDKElement"></div>
    
    <script>
        var authEndpoint = 'https://your-server.com/api/zoom/signature';
        var sdkKey = 'your-sdk-key';
        var meetingNumber = 123456789;
        var passWord = 'meeting-password';
        var role = 0; // 0: 参加者, 1: ホスト
        var userName = 'User Name';
        var userEmail = '[email protected]';
        var registrantToken = '';
        var zakToken = '';

        document.getElementById('meetingSDKElement').style.display = 'block';

        ZoomMtg.setZoomJSLib('https://source.zoom.us/2.16.0/lib', '/av');

        ZoomMtg.preLoadWasm();
        ZoomMtg.prepareWebSDK();

        // リモートからJWT署名を取得
        function getSignature() {
            fetch(authEndpoint, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    meetingNumber: meetingNumber,
                    role: role
                })
            }).then((response) => {
                return response.json();
            }).then((data) => {
                console.log(data);
                startMeeting(data.signature);
            }).catch((error) => {
                console.error(error);
            });
        }

        function startMeeting(signature) {
            ZoomMtg.init({
                leaveUrl: 'https://your-website.com',
                success: (success) => {
                    console.log(success);

                    ZoomMtg.join({
                        signature: signature,
                        meetingNumber: meetingNumber,
                        userName: userName,
                        sdkKey: sdkKey,
                        userEmail: userEmail,
                        passWord: passWord,
                        tk: registrantToken,
                        zak: zakToken,
                        success: (success) => {
                            console.log('Join meeting success');
                        },
                        error: (error) => {
                            console.log(error);
                        }
                    });
                },
                error: (error) => {
                    console.log(error);
                }
            });
        }

        getSignature();
    </script>
</body>
</html>

Webhook処理

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Webhook署名検証
function verifyWebhook(payload, headers) {
  const message = `v0:${headers['x-zm-request-timestamp']}:${payload}`;
  const hashForVerify = crypto.createHmac('sha256', process.env.ZOOM_WEBHOOK_SECRET)
    .update(message)
    .digest('hex');
  const signature = `v0=${hashForVerify}`;
  
  return headers['x-zm-signature'] === signature;
}

// Webhookエンドポイント
app.post('/zoom/webhook', (req, res) => {
  const payload = JSON.stringify(req.body);
  
  // 署名検証
  if (!verifyWebhook(payload, req.headers)) {
    return res.status(401).send('Unauthorized');
  }

  const { event, payload: eventPayload } = req.body;

  switch (event) {
    case 'meeting.started':
      console.log('Meeting started:', eventPayload.object);
      handleMeetingStarted(eventPayload.object);
      break;
      
    case 'meeting.ended':
      console.log('Meeting ended:', eventPayload.object);
      handleMeetingEnded(eventPayload.object);
      break;
      
    case 'meeting.participant_joined':
      console.log('Participant joined:', eventPayload.object);
      handleParticipantJoined(eventPayload.object);
      break;
      
    case 'meeting.participant_left':
      console.log('Participant left:', eventPayload.object);
      handleParticipantLeft(eventPayload.object);
      break;
      
    case 'recording.completed':
      console.log('Recording completed:', eventPayload.object);
      handleRecordingCompleted(eventPayload.object);
      break;
      
    default:
      console.log('Unhandled event:', event);
  }

  res.status(200).send('OK');
});

// イベント処理関数
async function handleMeetingStarted(meeting) {
  // 会議開始時の処理
  console.log(`Meeting "${meeting.topic}" started at ${meeting.start_time}`);
  
  // Slack通知例
  await sendSlackNotification({
    text: `🚀 Meeting "${meeting.topic}" has started!`,
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Meeting Started*\n*Topic:* ${meeting.topic}\n*ID:* ${meeting.id}\n*Host:* ${meeting.host_email}`
        }
      }
    ]
  });
}

async function handleMeetingEnded(meeting) {
  // 会議終了時の処理
  console.log(`Meeting "${meeting.topic}" ended at ${meeting.end_time}`);
  
  const duration = new Date(meeting.end_time) - new Date(meeting.start_time);
  const minutes = Math.floor(duration / 60000);
  
  await sendSlackNotification({
    text: `✅ Meeting "${meeting.topic}" has ended (${minutes} minutes)`,
  });
}

async function handleRecordingCompleted(recording) {
  // 録画完了時の処理
  console.log('Recording files:', recording.recording_files);
  
  for (const file of recording.recording_files) {
    if (file.file_type === 'MP4') {
      console.log(`Video recording available: ${file.download_url}`);
      
      // 録画ファイルのダウンロードとアップロード
      await processRecording(file);
    }
  }
}

async function processRecording(recordingFile) {
  // 録画ファイル処理
  const response = await fetch(recordingFile.download_url, {
    headers: {
      'Authorization': `Bearer ${process.env.ZOOM_JWT}`
    }
  });
  
  if (response.ok) {
    // ファイルを外部ストレージ(S3など)にアップロード
    console.log('Recording processed successfully');
  }
}

app.listen(3000, () => {
  console.log('Zoom webhook server running on port 3000');
});

Apps for Zoom 開発

// Apps for Zoom アプリケーション例
const express = require('express');
const app = express();

app.use(express.static('public'));
app.use(express.json());

// Zoom Apps認証
app.get('/authorize', (req, res) => {
  const { code, state } = req.query;
  
  if (code) {
    // アクセストークンを取得
    exchangeCodeForToken(code)
      .then(tokenData => {
        // トークンを保存
        res.redirect('/app.html');
      })
      .catch(error => {
        console.error('Authorization error:', error);
        res.status(500).send('Authorization failed');
      });
  } else {
    res.status(400).send('Authorization code not provided');
  }
});

// Zoom Apps API エンドポイント
app.post('/api/meeting-info', async (req, res) => {
  try {
    // 現在の会議情報を取得
    const meetingContext = await zoomSdk.getMeetingContext();
    
    res.json({
      meetingId: meetingContext.meetingId,
      participants: meetingContext.participants,
      duration: meetingContext.duration
    });
  } catch (error) {
    console.error('Error getting meeting info:', error);
    res.status(500).json({ error: 'Failed to get meeting info' });
  }
});

// Zoom Apps クライアントサイド(public/app.html)
const zoomAppScript = `
<script src="https://appssdk.zoom.us/sdk.min.js"></script>
<script>
  // Zoom Apps SDK初期化
  zoomSdk.config({
    capabilities: ['getRunningContext', 'getMeetingContext'],
    version: '0.16.0'
  }).then(() => {
    console.log('Zoom Apps SDK initialized');
    loadMeetingData();
  }).catch((error) => {
    console.error('SDK initialization failed', error);
  });

  async function loadMeetingData() {
    try {
      // 実行コンテキスト取得
      const runningContext = await zoomSdk.getRunningContext();
      console.log('Running context:', runningContext);

      // 会議コンテキスト取得
      const meetingContext = await zoomSdk.getMeetingContext();
      console.log('Meeting context:', meetingContext);

      // UI更新
      document.getElementById('meetingId').textContent = meetingContext.meetingId;
      document.getElementById('participants').textContent = meetingContext.participants?.length || 0;
      
    } catch (error) {
      console.error('Error loading meeting data:', error);
    }
  }

  // 参加者リスト更新
  async function updateParticipants() {
    try {
      const participants = await zoomSdk.getMeetingParticipants();
      const participantsList = document.getElementById('participantsList');
      
      participantsList.innerHTML = participants
        .map(p => \`<li>\${p.displayName} - \${p.role}</li>\`)
        .join('');
        
    } catch (error) {
      console.error('Error updating participants:', error);
    }
  }

  // イベントリスナー設定
  zoomSdk.addEventListener('onParticipantChange', (event) => {
    console.log('Participant change:', event);
    updateParticipants();
  });

  zoomSdk.addEventListener('onMeetingConfigChanged', (event) => {
    console.log('Meeting config changed:', event);
  });
</script>
`;

Cloud Recording API

class ZoomRecordingAPI {
  constructor(jwt) {
    this.jwt = jwt;
    this.baseURL = 'https://api.zoom.us/v2';
  }

  // 録画一覧取得
  async getRecordings(userId, from, to) {
    try {
      const response = await axios.get(
        `${this.baseURL}/users/${userId}/recordings`,
        {
          params: {
            from,
            to,
            page_size: 300
          },
          headers: {
            'Authorization': `Bearer ${this.jwt}`
          }
        }
      );
      
      return response.data;
    } catch (error) {
      console.error('Error fetching recordings:', error.response.data);
      throw error;
    }
  }

  // 録画詳細取得
  async getRecording(meetingId) {
    try {
      const response = await axios.get(
        `${this.baseURL}/meetings/${meetingId}/recordings`,
        {
          headers: {
            'Authorization': `Bearer ${this.jwt}`
          }
        }
      );
      
      return response.data;
    } catch (error) {
      console.error('Error fetching recording details:', error.response.data);
      throw error;
    }
  }

  // 録画ダウンロード
  async downloadRecording(downloadUrl, outputPath) {
    try {
      const response = await axios({
        method: 'GET',
        url: downloadUrl,
        responseType: 'stream',
        headers: {
          'Authorization': `Bearer ${this.jwt}`
        }
      });

      const writer = fs.createWriteStream(outputPath);
      response.data.pipe(writer);

      return new Promise((resolve, reject) => {
        writer.on('finish', resolve);
        writer.on('error', reject);
      });
    } catch (error) {
      console.error('Error downloading recording:', error);
      throw error;
    }
  }

  // 録画削除
  async deleteRecording(meetingId) {
    try {
      await axios.delete(
        `${this.baseURL}/meetings/${meetingId}/recordings`,
        {
          headers: {
            'Authorization': `Bearer ${this.jwt}`
          }
        }
      );
      
      return { success: true };
    } catch (error) {
      console.error('Error deleting recording:', error.response.data);
      throw error;
    }
  }
}

// 使用例
const recordingAPI = new ZoomRecordingAPI(process.env.ZOOM_JWT);

async function processRecentRecordings() {
  const fromDate = '2024-01-01';
  const toDate = '2024-01-31';
  
  try {
    const recordings = await recordingAPI.getRecordings('[email protected]', fromDate, toDate);
    
    for (const meeting of recordings.meetings) {
      console.log(`Processing recordings for meeting: ${meeting.topic}`);
      
      for (const file of meeting.recording_files) {
        if (file.file_type === 'MP4') {
          const outputPath = `./recordings/${meeting.id}_${file.id}.mp4`;
          await recordingAPI.downloadRecording(file.download_url, outputPath);
          console.log(`Downloaded: ${outputPath}`);
        }
      }
    }
  } catch (error) {
    console.error('Failed to process recordings:', error);
  }
}

環境変数設定

# .env ファイル
ZOOM_JWT=your-zoom-jwt-token
ZOOM_API_KEY=your-zoom-api-key
ZOOM_API_SECRET=your-zoom-api-secret
ZOOM_WEBHOOK_SECRET=your-webhook-secret-token

# SDK設定
ZOOM_SDK_KEY=your-sdk-key
ZOOM_SDK_SECRET=your-sdk-secret

# Zoom Apps設定
ZOOM_APP_CLIENT_ID=your-app-client-id
ZOOM_APP_CLIENT_SECRET=your-app-client-secret
ZOOM_APP_REDIRECT_URL=https://your-app.com/authorize

# 外部連携
SLACK_WEBHOOK_URL=your-slack-webhook-url
AWS_S3_BUCKET=your-s3-bucket-name

Docker設定例

FROM node:18-alpine

WORKDIR /app

# 依存関係のインストール
COPY package*.json ./
RUN npm ci --only=production

# アプリケーションのコピー
COPY . .

# 環境変数
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

# アプリケーション実行
CMD ["npm", "start"]