LoginSignup
3
1
新しくなったSkyWayを使ってみよう!

新しくなったSkyWayをkintoneで試してみた

Last updated at Posted at 2023-07-17

SkyWayが新しくなったとのことでサンプルコードを元にkintoneに組み込んでみました。

kintoneでビデオ会議は需要あると思うんですけどどうなんでしょう。
ちなみにkintoneの開発者ライセンスは無料なので持ってない方は是非。

ほぼ以下のサンプルコードです。kintone用に少しだけ改変しました。
https://github.com/skyway/js-sdk/tree/main/examples/auto-subscribe

あらかじめSkyWayのアプリケーションIDとシークレットキーはご用意ください。

まずはkintoneのアプリにSkyWayのコントロール要素を全てぶち込むために「video-container」という要素IDのスペースを作成してください。
スクリーンショット 2023-07-15 061333.jpg

今回はskywayのライブラリをCDNで以下を読み込みます
https://cdn.jsdelivr.net/npm/@skyway-sdk/room/dist/skyway_room-latest.js

スクリーンショット 2023-07-15 061206.jpg

コード解説はchatGPTにお願いしました。

script.js
const { nowInSec, SkyWayAuthToken, SkyWayContext, SkyWayRoom, SkyWayStreamFactory, uuidV4 } = skyway_room;

const token = new SkyWayAuthToken({
    jti: uuidV4(),
    iat: nowInSec(),
    exp: nowInSec() + 60 * 60 * 24,
    scope: {
        app: {
            id: 'アプリケーションIDをここに入力',
            turn: true,
            actions: ['read'],
            channels: [
                {
                    id: '*',
                    name: '*',
                    actions: ['write'],
                    members: [
                        {
                            id: '*',
                            name: '*',
                            actions: ['write'],
                            publication: {
                                actions: ['write'],
                            },
                            subscription: {
                                actions: ['write'],
                            },
                        },
                    ],

                    sfuBots: [
                        {
                            actions: ['write'],
                            forwardings: [{ actions: ['write'] }],
                        },
                    ],
                },
            ],
        },
    },
}).encode('シークレットキーをここに入力');

kintone.events.on('app.record.detail.show', (event) => {
    (async () => {

        let videoContainer = kintone.app.record.getSpaceElement('video-container');
        const recordId = kintone.app.record.getId();
        const user = kintone.getLoginUser();

        // joinTrigger
        const joinTrigger = document.createElement('div');
        joinTrigger.id = 'js-join-trigger';
        joinTrigger.innerText = 'ルームに参加';
        videoContainer.appendChild(joinTrigger);

        // leaveTrigger
        const leaveTrigger = document.createElement('div');
        leaveTrigger.id = 'js-leave-trigger';
        leaveTrigger.innerText = 'ルームを退出';
        videoContainer.appendChild(leaveTrigger);

        // channelName
        const channelName = document.createElement('div');
        channelName.id = 'js-channel-name';
        videoContainer.appendChild(channelName);

        // roomMode
        const roomMode = document.createElement('div');
        roomMode.id = 'js-room-type';
        // select要素を作成
        const selectElement = document.createElement("select");

        // option要素を作成してselect要素に追加
        const option1 = document.createElement("option");
        option1.text = "sfu";
        option1.value = "sfu";
        selectElement.add(option1);

        const option2 = document.createElement("option");
        option2.text = "p2p";
        option2.value = "p2p";
        selectElement.add(option2);


        roomMode.appendChild(selectElement);
        videoContainer.appendChild(roomMode);
        
        // messages
        const messages = document.createElement('div');
        messages.id = 'js-messages';
        videoContainer.appendChild(messages);

        // localStream
        const localStream = document.createElement('video');
        localStream.id = 'js-local-stream';
        videoContainer.appendChild(localStream);
        
        // remoteVideos
        const remoteVideos = document.createElement('div');
        remoteVideos.id = 'js-remote-streams';
        videoContainer.appendChild(remoteVideos);

        // 初期値を設定
        let selectedValue = "sfu";
        // イベントリスナーを追加して選択された値を取得
        selectElement.addEventListener("change", function() {
            selectedValue = selectElement.value;
        });


        const localVideo = document.getElementById('js-local-stream');

        const { audio, video } =
            //マイクとカメラからのオーディオとビデオのストリームを作成します。
            //戻り値は、オーディオとビデオのストリームが含まれたオブジェクトです。
            await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream();

        //ビデオ要素の音声をミュートに設定しています。
        localVideo.muted = true;
        //ビデオをインライン再生するための設定です。
        localVideo.playsInline = true;
        //取得したビデオストリームを video 要素にアタッチします。
        video.attach(localVideo);
        //ビデオの再生を開始しています。
        await localVideo.play();

        //コンテキスト(制御情報)の作成
        const context = await SkyWayContext.Create(token, {
            log: { level: 'warn', format: 'object' },
        });

        let room;

        // Register join handler
        joinTrigger.addEventListener('click', async () => {
            if (room) {
                return;
            }
            //コンテキストを使用してルームを検索し、存在しない場合は新しいルームを作成します。
            room = await SkyWayRoom.FindOrCreate(context, {
                name: 'room' + recordId,
                type: selectedValue,
            });

            //作成または検索されたルームに参加します。
            const member = await room.join();
            messages.textContent += '=== You joined ===\n';

            //メンバーがルームに参加したときに実行される関数
            room.onMemberJoined.add((e) => {
                messages.textContent += `=== ${e.member.id.slice(0, 5)} joined ===\n`;
            });

            const userVideo = {};

            //このMemberがStreamをSubscribeしたときに発火するイベント
            member.onPublicationSubscribed.add(async ({ stream, subscription }) => {
                //データチャネルの場合は、処理を終了します。
                if (stream.contentType === 'data') return;

                //パブリッシャー(ストリームを公開したメンバー)のIDを取得
                const publisherId = subscription.publication.publisher.id;
                //パブリッシャーのIDが存在しない場合は、処理を終了します。
                if (!userVideo[publisherId]) {
                    //新しいvideo要素を作成します。
                    const newVideo = document.createElement('video');
                    //ビデオをインライン再生するための設定です。
                    newVideo.playsInline = true;
                    //ビデオの再生を開始しています。
                    newVideo.autoplay = true;
                    //パブリッシャーのIDをdata-member-id属性に設定しています。
                    newVideo.setAttribute(
                        'data-member-id',
                        subscription.publication.publisher.id
                    );
                    //新しいvideo要素をremoteVideosに追加します。
                    remoteVideos.append(newVideo);
                    //userVideoにパブリッシャーのIDをキーとして、新しいvideo要素を設定します。
                    userVideo[publisherId] = newVideo;
                }
                //userVideoに設定された新しいvideo要素を取得します。
                const newVideo = userVideo[publisherId];
                //取得したストリームを新しいvideo要素にアタッチします。
                stream.attach(newVideo);
                //subscription.contentTypeが 'video'であり、room.typeが 'sfu'である場合
                if (subscription.contentType === 'video' && room.type === 'sfu') {
                    //新しいvideo要素をクリックしたときに実行される関数
                    newVideo.onclick = () => {
                        if (subscription.preferredEncoding === 'low') {
                            subscription.changePreferredEncoding('high');
                        } else {
                            subscription.changePreferredEncoding('low');
                        }
                    };
                }
            });
            //のストリームを購読するための関数
            const subscribe = async (publication) => {
                //自身が公開したストリームを購読することを避ける    
                if (publication.publisher.id === member.id) return;
                //購読する
                await member.subscribe(publication.id);
            };
            //room.onStreamPublishedに購読する関数を追加します。
            room.onStreamPublished.add((e) => subscribe(e.publication));
            //room.publicationsに購読する関数を追加します。
            room.publications.forEach(subscribe);
            //メンバーがルームから離脱したときに実行される関数
            await member.publish(audio);
            //room.typeが 'sfu'である場合
            if (room.type === 'sfu') {
                //メンバーがルームに参加したときに実行される関数
                await member.publish(video, {
                    //エンコーディングの設定
                    encodings: [
                        //最大ビットレートが 10,000 であるエンコーディングを設定します。
                        { maxBitrate: 10_000, id: 'low' },
                        //最大ビットレートが 800,000 であるエンコーディングを設定します。
                        { maxBitrate: 800_000, id: 'high' },
                    ],
                });
            } else {
                //メンバーがルームに参加したときに実行される関数
                await member.publish(video);
            }
            //ビデオ要素を破棄するための関数
            const disposeVideoElement = (remoteVideo) => {
                //メディアストリームを取得
                const stream = remoteVideo.srcObject;
                //メディアストリームのトラックを停止
                stream.getTracks().forEach((track) => track.stop());
                //メディアストリームを破棄
                remoteVideo.srcObject = null;
                //ビデオ要素を削除
                remoteVideo.remove();
            };
            //メンバーがルームから離脱したときに実行される関数
            room.onMemberLeft.add((e) => {
                //自身がルームから離脱した場合は、処理を終了します。
                if (e.member.id === member.id) return;
                //remoteVideosからdata-member-id属性がe.member.idの要素を取得します。
                const remoteVideo = remoteVideos.querySelector(
                    `[data-member-id="${e.member.id}"]`
                );
                //remoteVideoが存在しない場合は、処理を終了します。
                disposeVideoElement(remoteVideo);
                //メッセージを表示します。
                messages.textContent += `=== ${e.member.id.slice(0, 5)} left ===\n`;
            });
            //メンバーがルームから離脱したときに実行される関数
            member.onLeft.once(() => {
                //remoteVideosの子要素を取得します。
                Array.from(remoteVideos.children).forEach((element) => {
                    //ビデオ要素を破棄する
                    disposeVideoElement(element);
                });
                //メッセージを表示します。
                messages.textContent += '== You left ===\n';
                //ルームを破棄します。
                room.dispose();
                //roomをundefinedに設定します。
                room = undefined;
            });
            //メンバーがルームから離脱したときに実行される関数
            leaveTrigger.addEventListener('click', () => member.leave(), {
                once: true,
            });
        });
    })();
});

CSSも少しだけ

style.css

#js-join-trigger {
    width: 100px;
    background-color: #000;
    color: #fff;
    text-align: center;
    padding: 10px;
    display: inline-block;
    cursor: pointer;
    margin: 0px 10px 20px 0px;
}
#js-leave-trigger {
    width: 100px;
    background-color: #000;
    color: #fff;
    text-align: center;
    padding: 10px;
    display: inline-block;
    cursor: pointer;
    margin: 0px 10px 20px 0px;
}
#js-room-type {
    margin: 0px 10px 20px 0px;
}
#js-local-stream {
    width: 400px;
}
#js-local-stream {
    width: 400px;
}
#js-remote-streams {
    width: 400px;
}
#js-messages {
    white-space: pre-wrap;
}


スクリーンショット 2023-07-17 200312.jpg

動いた!ばんざーい

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1