SkyWayを簡単に書きたい


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

```typescript
import {
  SkyWayAuthToken,
  uuidV4,
  nowInSec,
  SkyWayStreamFactory,
  SkyWayContext,
  SkyWayRoom,
} from "@skyway-sdk/room";

```

序盤も序盤に「こんなにいるの？」と。

# どう書きたいか

※コードはイメージです

```typescript
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/

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


```