1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SkyWayを簡単に書きたい

Last updated at Posted at 2023-07-21

わたくしには無理そう、と思ったところ

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 };
  }
}


1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?