わたくしには無理そう、と思ったところ
import {
SkyWayAuthToken,
uuidV4,
nowInSec,
SkyWayStreamFactory,
SkyWayContext,
SkyWayRoom,
} from "@skyway-sdk/room";
序盤も序盤に「こんなにいるの?」と。
どう書きたいか
※コードはイメージです
import { SmartSkyWay } from '...'; // importは1つだけ
const ssw = new SmartSkyWay(token);
// 部屋に関すること
await ssw.room.prepare('ルーム名');
await ssw.room.join(handleRecieve); //相手streamの受取処理を渡す
// 自分側に関すること
await ssw.local.prepare('自画面のvideoタグへのCSSセレクタ');
await ssw.local.publish();
これくらいで繋がってほしい!
チュートリアルを修正
かなり短くなりました
https://skyway.ntt.com/ja/docs/user-guide/javascript-sdk/quickstart/
import { SmartSkyWay } from "./SmartSkyWay"; //importは1つ
import type { RoomPublication, LocalStream } from "@skyway-sdk/room"; //typeもあるけど...
//このへんは、まぁ、しゃあなしか...
const buttonArea = document.getElementById("button-area")!;
const remoteMediaArea = document.getElementById("remote-media-area")!;
const roomNameInput = document.querySelector<HTMLInputElement>("#room-name")!;
const myId = document.getElementById("my-id")!;
const joinButton = document.getElementById("join")!;
const token = SmartSkyWay.getToken({
id: "ここにアプリケーションIDをペーストしてください",
secret: "ここにシークレットキーをペーストしてください",
});
const ssw = new SmartSkyWay(token);
ssw.local.prepare("#local-video");
//相手streamの受取処理を用意
const handleRecieve = (publication: RoomPublication<LocalStream>) => {
const subscribeButton = document.createElement("button");
subscribeButton.textContent = `${publication.publisher.id}: ${publication.contentType}`;
buttonArea.appendChild(subscribeButton);
subscribeButton.onclick = async () => {
const { element } = await ssw.local.subscribe(publication.id);
remoteMediaArea.appendChild(element);
};
};
joinButton.onclick = async () => {
if (roomNameInput.value === "") return;
await ssw.room.prepare(roomNameInput.value);
await ssw.room.join(handleRecieve);
myId.textContent = ssw.local.props?.id || "";
await ssw.local.publish();
};
まだまだ足りない、、、
キャンペーンの期限がきたので一旦、イメージしているものを投稿しますが、
まだ求めているところに届いていないので、継続して改善していきたいと思います。
https://qiita.com/official-events/93564ad363199fa7999c
ただ、処理をまとめていくだけだと、1つのメソッドに多数の意味を持たせることになっていき
これはこれでアンチパターンと思うので、主要な処理を見極めつつまとめてみたいところです。
すそ野こそ数が多い
ユーザが増えればエコシステムが強化され、
足りないところをOSSで支え合える状態になり
SkyWayが使いやすくなっていくのかなと思うのですが
私のような未熟者には生のSDKはちょっと敷居が高い感じがしました。
簡単に使えるようなライブラリがあると、より多くのユーザに支持される可能性があるのでは、
という視点から今回の記事を書いてみました。
未熟者でなくても大半は、SkyWayのエンジニアになりたいのではなく
SkyWayで課題を解決したいと思っているはずで、
課題解決にSkyWayが最短となれるような、
分かりやすく簡単に使える状態になれば良いなと思います。
現状の案
SmartSkyWay.ts
import {
LocalAudioStream,
LocalP2PRoomMember,
LocalStream,
LocalVideoStream,
nowInSec,
P2PRoom,
RoomPublication,
SkyWayAuthToken,
SkyWayContext,
SkyWayRoom,
SkyWayStreamFactory,
uuidV4,
} from "@skyway-sdk/room";
type Props = {
id: string;
secret: string;
exp?: number;
scope?: typeof defaultScope;
};
const defaultScope = {
app: {
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"],
},
],
},
],
},
],
},
};
export class SmartSkyWay {
public static getToken({ id, secret, exp = 60 * 60 * 24, scope }: Props) {
return new SkyWayAuthToken({
jti: uuidV4(),
iat: nowInSec(),
exp: nowInSec() + exp,
scope: scope || { ...defaultScope, app: { ...defaultScope.app, id } },
}).encode(secret);
}
private token = "";
private roomName = "";
private type: "p2p" /* | "sfu" */ = "p2p";
private context: SkyWayContext | null = null;
private _room: P2PRoom | null = null;
private me: LocalP2PRoomMember | null = null;
private audio: LocalAudioStream | null = null;
private video: LocalVideoStream | null = null;
private onRecieve: (publication: RoomPublication<LocalStream>) => void;
constructor(token: string) {
if (!token) throw new Error("no token");
this.token = token;
}
public get local() {
return {
props: this.me,
prepare: (query: string) => this.prepareLocalStream(query),
publish: (arg?: { audio: boolean; video: boolean }) => this.publish(arg),
subscribe: (publicationId: string) => this.subscribe(publicationId),
};
}
public get room() {
return {
props: this._room,
join: (onRecieve: (publication: RoomPublication<LocalStream>) => void) =>
this.joinRoom(onRecieve),
prepare: (roomName: string, type: "p2p" /*| "sfu"*/ = "p2p") =>
this.prepareRoom(roomName, type),
};
}
private async prepareLocalStream(query: string) {
const videoElem = document.querySelector<HTMLVideoElement>(query);
if (!videoElem) throw new Error("no element");
const stream =
await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream();
this.audio = stream.audio;
this.video = stream.video;
this.video.attach(videoElem);
await videoElem.play();
}
private async prepareRoom(roomName: string, type: "p2p" /*| "sfu"*/ = "p2p") {
if (!roomName) throw new Error("no roomName");
this.roomName = roomName;
this.type = type;
this.context = await SkyWayContext.Create(this.token);
this._room = await SkyWayRoom.FindOrCreate(this.context, {
type: this.type,
name: this.roomName,
});
}
private async joinRoom(
onRecieve: (publication: RoomPublication<LocalStream>) => void
) {
if (!this._room) throw new Error("no room");
this.me = await this._room.join();
this.onRecieve = (publication: RoomPublication<LocalStream>) => {
if (publication.publisher.id === this.me?.id) return;
onRecieve(publication);
};
this._room.publications.forEach(this.onRecieve);
this._room.onStreamPublished.add((e) => onRecieve(e.publication));
}
private async publish(arg?: { audio: boolean; video: boolean }) {
if (!arg || arg.audio) {
if (!this.audio) {
throw new Error("no local audio. call attachLocalVideo()");
}
await this.me?.publish(this.audio);
}
if (!arg || arg.video) {
if (!this.video) {
throw new Error("no local video. call attachLocalVideo()");
}
await this.me?.publish(this.video);
}
}
private async subscribe(publicationId: string) {
if (!this.me) throw new Error("no me. call room.join()");
const { stream } = await this.me.subscribe(publicationId);
let element: HTMLVideoElement | HTMLAudioElement;
switch (stream.track.kind) {
case "video":
const v = document.createElement("video");
v.playsInline = true;
v.autoplay = true;
element = v;
break;
case "audio":
const a = document.createElement("audio");
a.controls = true;
a.autoplay = true;
element = a;
break;
// case "data": ?
default:
throw new Error(`invalid stream track kind: ${stream.track.kind}`);
}
stream.attach(element);
return { element, stream };
}
}