見出し画像

【5/5ページ】SkyWay SDK for Linuxハンズオン資料 まとめ編

このコンテンツはSDK for Linux®︎のハンズオンイベントで利用するコンテンツです。
ハンズオンに参加できなかった方でも、個人で本記事に沿って作成いただけるように構成しています。
ハンズオンの概要・使用する機材の情報については概要ページをご覧ください。


まとめ

ここまでで、LEDの状態をカメラで確認しながら、その制御を行うアプリを作成しました。
今回はローカルな環境で動作確認を行いましたが、遠隔地からでもインターネットにさえ繋がっていればどこでもLEDを制御できます。

今回作成したアプリではカメラ映像を送信するのみでしたが、他にも音声やテキストメッセージなどを送信することもできます。
受信も同様です。
例えば、Raspberry Piにスピーカーをつけてペットカメラのように遠隔地に音声を届けることができます。
モーターと繋げて餌やりの機構を作ることもできるかもしれません。
また、今回はSkyWay Explorerを利用してブラウザ側の操作をしましたが、自分で対向側のアプリを作ることもできます。
ブラウザだけに限らず、Android・iOSのアプリも作ることもできます。

ここまでお読みいただき、またハンズオンをお試しいただき、ありがとうございました。

付録

他のプラットフォームのクイックスタート

他の内容も試してみたい方は是非SkyWayの公式サイトから各SDKのQuickStartを試してみてください。

JavaScript SDK

iOS SDK

Android SDK

SDK for Linux®︎

Unity SDK (β版)

完成版のソース

# CMakeLists.txt

cmake_minimum_required(VERSION 3.10.2)

project("handson")

# clang++/C++17を利用します。
enable_language(CXX)
set(CMAKE_CXX_COMPILER "/usr/bin/clang++" CACHE STRING "clang++ compile" FORCE)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# libsにある各ライブラリを読み込みます。
add_library( skyway-linux-core STATIC IMPORTED )
set_target_properties( skyway-linux-core
        PROPERTIES
        IMPORTED_LOCATION
        ${CMAKE_CURRENT_SOURCE_DIR}/libs/libskyway-linux-core.a)

add_library( skyway-linux-room STATIC IMPORTED )
set_target_properties( skyway-linux-room
        PROPERTIES
        IMPORTED_LOCATION
        ${CMAKE_CURRENT_SOURCE_DIR}/libs/libskyway-linux-room.a)

# SkyWayを利用する際に必要な定義します。
set( SKYWAY_LINUX_DEFINITIONS
        WEBRTC_LINUX=1 
        WEBRTC_POSIX=1)

# アプリで利用する依存ライブラリです。
set( SKYWAY_LINUX_LIBS
        skyway-linux-room
        skyway-linux-core
        pthread
        atomic 
        avcodec 
        X11 
        glib-2.0 
        gio-2.0
        gobject-2.0
        dl
        gpiod)

# アプリで利用するincludeパスを設定します。
set( SKYWAY_LINUX_INCLUDES 
        ${CMAKE_CURRENT_SOURCE_DIR}/include/external/libmediasoupclient/include
        ${CMAKE_CURRENT_SOURCE_DIR}/include/external/libmediasoupclient/deps/libsdptransform/include
        ${CMAKE_CURRENT_SOURCE_DIR}/include/external/libwebrtc
        ${CMAKE_CURRENT_SOURCE_DIR}/include/external/libskyway/include
        ${CMAKE_CURRENT_SOURCE_DIR}/include/external/libwebrtc/third_party/abseil-cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/include/external/libwebrtc/third_party/boringssl/src/include
        ${CMAKE_CURRENT_SOURCE_DIR}/include/external/boost
        ${CMAKE_CURRENT_SOURCE_DIR}/include)

add_executable(app.out main.cpp handson_room.cpp)
target_compile_definitions(app.out PRIVATE ${SKYWAY_LINUX_DEFINITIONS})
target_include_directories(app.out PRIVATE ${SKYWAY_LINUX_INCLUDES})
target_link_libraries(app.out ${SKYWAY_LINUX_LIBS})

// main.cpp

#include <iostream>

#include "handson_room.hpp"

int main(int argc, char* argv[]) {
    // 実行時引数にroom_nameを取るようにします。
    if (argc != 2) {
        std::cerr << "app.out <room_name>" << std::endl;
        return -1;
    }

    // 第一引数をRoomNameとします。
    std::string room_name = argv[1];

    // [1] SkyWay Auth Tokenを取得します。
    const char* token = getenv("SKYWAY_AUTH_TOKEN");
    if (!token) {
        std::cerr << "Please set SKYWAY_AUTH_TOKEN environment variable." << std::endl;
        return -1;
    }

    // [2]SkyWayとGPIOのセットアップを行います。
    // SkyWayの詳細なログの出力を表示したい場合はLogLevelをkInfoなどに変更してください。
    auto skyway_log_level = skyway::global::interface::Logger::kError;
    
    // handson_room内に実装されているログ出力が必要な場合にはtrueに変更してください。
    auto app_is_notify = false;

    // 使用するGPIOピン番号を設定します。
    auto gpio_line_number = 26;
    
    auto handson_room = HandsOnRoom(app_is_notify);
    if (!handson_room.Setup(token,gpio_line_number,skyway_log_level)) {
        return -1;
    }

    // [3] RoomにJoinします。
    if (!handson_room.JoinRoom(room_name)) {
        return -1;
    }

    // [4] カメラ映像をPublishします。
    handson_room.PublishCamera();
    
    // [5] DataStreamをSubscribeします。
    handson_room.SubscribeAllDataStream();
    
    // キー入力を待ちます。
    std::cout << "- Press ENTER key to close..." << std::endl;
    std::cin.get();

    // [6] Roomから退出します。
    handson_room.LeaveRoom();

    // [7] リソースを破棄します。
    handson_room.Dispose();
    return 0;
}

// handson_room.hpp

#ifndef SKYWAY_HANDSON_ROOM_HPP_
#define SKYWAY_HANDSON_ROOM_HPP_

#include <gpiod.h>

#include <skyway/context.hpp>
#include <skyway/core/stream/remote/data_stream.hpp>
#include <skyway/media/device_manager.hpp>
#include <skyway/media/stream_factory.hpp>
#include <skyway/media/v4l2_video_renderer.hpp>
#include <skyway/room/p2p/p2p_room.hpp>

// Roomの操作を行うクラスです。
// イベントリスナーはこのクラスに実装します。
// 
// `Room::EventListener`ではRoomで発生したイベントを受け取ることができます。
// 誰かが入室した、誰かがPublishしたなどです。
//
// `RemoteDataStream::Listener`では、データの受信に成功すると発火するイベントがあります。
class HandsOnRoom : public skyway::room::interface::Room::EventListener,
                    public skyway::core::stream::remote::RemoteDataStream::Listener {
public:
    // [a] コンストラクタとアプリのセットアップを行います。
    HandsOnRoom(bool is_notify);

    // GPIOの初期化およびSkyWayのセットアップを行います。
    bool Setup(const std::string& token,const int gpio_line_number,skyway::global::interface::Logger::Level log_level);

    // [b] RoomにJoinします。
    bool JoinRoom(const std::string& room_name);
    
    // [c] カメラ映像をPublishします。
    void PublishCamera();

    // [d] DataStreamをSubscribeします。
    bool SubscribeDataStream(std::unique_ptr<skyway::room::interface::RoomPublication> publication);
    void SubscribeAllDataStream();

    // [e] 指定のPublicationをSubscribeしているかチェックします。
    bool IsSubscribed(skyway::room::interface::RoomPublication* publication);

    // [f] Roomイベントのリスナ実装します。
    // Room内の誰かがPublishするとこのイベントが発火します。
    void OnStreamPublished(
        std::unique_ptr<skyway::room::interface::RoomPublication> publication) override;

    // [g] DataStreamのイベントのリスナ実装
    // 文字列を受信すると発火するイベントです。
    void OnData(const std::string& data) override;

    // バイナリデータを受信すると発火するイベントです。
    // 今回は使用しません。
    void OnDataBuffer(const uint8_t* data, size_t length) override;

    // [h] Roomから退出します。
    bool LeaveRoom();

    // [i] リソースを破棄します。
    void Dispose();

private:
    // 今回利用するメンバー変数です。
    std::shared_ptr<skyway::room::p2p::P2PRoom> p2proom_;
    std::unique_ptr<skyway::room::p2p::LocalP2PRoomMember> room_member_;
    std::vector<std::unique_ptr<std::thread>> threads_;
    bool is_leaving_;
    bool is_notify_;
    struct gpiod_chip *gpio_chip_;
    struct gpiod_line *gpio_line_out_;
};
#endif  // SKYWAY_HANDSON_ROOM_HPP_

// handson_room.cpp

#include "handson_room.hpp"

#include <iostream>

// [A] コンストラクタとアプリのセットアップを行います。
HandsOnRoom::HandsOnRoom(bool is_notify)
    : p2proom_(nullptr),
      room_member_(nullptr),
      threads_(std::vector<std::unique_ptr<std::thread>>()),
      is_leaving_(false),
      is_notify_(is_notify),
      gpio_chip_(nullptr),
      gpio_line_out_(nullptr) {}

// GPIOの初期化およびSkyWayのセットアップを行います。
bool HandsOnRoom::Setup(const std::string& token,const int gpio_line_number,skyway::global::interface::Logger::Level log_level) {
    gpio_chip_ = gpiod_chip_open("/dev/gpiochip4");
    gpio_line_out_ = gpiod_chip_get_line(gpio_chip_, gpio_line_number);
    gpiod_line_request_output(gpio_line_out_, "SkyWay HandsOn", 0);
    
    skyway::Context::SkyWayOptions context_options{};
    context_options.log_level = log_level;
    if (!skyway::Context::Setup(token, nullptr, context_options)) {
        std::cerr << "- [Error] setup failed." << std::endl;
        return false;
    }
    return true;
}

// [B] RoomにJoinします。
bool HandsOnRoom::JoinRoom(const std::string& room_name) {
    // P2PRoomを検索/作成します。
    skyway::room::interface::RoomInitOptions room_init;
    room_init.name = room_name;
    p2proom_       = skyway::room::p2p::P2PRoom::FindOrCreate(room_init);
    if (!p2proom_) {
        std::cerr << "- [Error] room failed." << std::endl;
        return false;
    }
    // P2PRoomにイベントリスナ(skyway::room::interface::Room::EventListenerの実装)を登録します。
    p2proom_->AddEventListener(this);

    // P2PRoomの情報を出力します。
    std::cout << "# Room" << std::endl;
    if (p2proom_->Name()) {
        std::cout << "- Name: " << p2proom_->Name().get() << std::endl;
    }
    std::cout << "- Id: " << p2proom_->Id() << std::endl;

    // P2PRoomに既にいるメンバーの一覧を表示します。
    std::cout << "- Room Members" << std::endl;
    for (auto& members : p2proom_->Members()) {
        std::cout << "  - Id: " << members->Id() << std::endl;
    }

    // P2PRoomにメンバーをJoinさせます。
    skyway::room::interface::RoomMemberInitOptions room_options;
    room_member_ = p2proom_->Join(room_options);
    if (!room_member_) {
        std::cerr << "- [Error] p2proom join failed." << std::endl;
        return false;
    }
    std::cout << "- LocalRoomMember Joined" << std::endl;
    std::cout << "  - Id: " << room_member_->Id() << std::endl;

    return true;
}

// [C] カメラ映像をPublishします。
void HandsOnRoom::PublishCamera() {
    auto video_devices = skyway::media::DeviceManager::GetVideoDevices();
    if (video_devices.size() > 0) {
        std::cout << "- VideoDevices" << std::endl;
        for (auto device : video_devices) {
            std::cout << "  - Index: " << device.index << " Name: " << device.name << std::endl;
        }

        // デバイスのindex番号を入力します。
        int device_index;
        std::cout << "- Enter the index of the video device to be published: ";
        std::cin >> device_index;
        std::cin.ignore();
        if (device_index >= 0 && device_index < video_devices.size()) {
            auto video_stream =
                skyway::media::StreamFactory::CreateVideoStream(video_devices[device_index]);
            skyway::room::interface::LocalRoomMember::PublicationOptions publication_options {};
            auto publication = room_member_->Publish(video_stream, publication_options);
            if (publication) {
                std::cout << "  - VideoStream Published" << std::endl;
                std::cout << "    - Publication Id: " << publication->Id() << std::endl;
            }
        } else {
            std::cout << "  - Out of range" << std::endl;
        }
    }
}

// [D] DataStreamをSubscribeします。
bool HandsOnRoom::SubscribeDataStream(std::unique_ptr<skyway::room::interface::RoomPublication> publication) {
    if (room_member_->Id() == publication->Publisher()->Id()) {
        // 自身がPublishしたPublicationはSubscribeできないので無視します。
        return false;
    }
    if (this->IsSubscribed(publication.get())) {
        // 既にSubscribeしている場合は無視します。
        return false;
    }

    // DataStreamをSubscribeします。
    if (publication->ContentType() == skyway::model::ContentType::kData) {
        skyway::room::interface::LocalRoomMember::SubscriptionOptions subscription_options {};
        auto subscription = room_member_->Subscribe(publication->Id(), subscription_options);
        if (!subscription) {
            return false;
        }
        auto data_stream =
            std::dynamic_pointer_cast<skyway::core::stream::remote::RemoteDataStream>(
                subscription->Stream());
        // P2PRoomにイベントリスナ(skyway::core::stream::remote::RemoteDataStream::Listenerの実装)を登録します。
        data_stream->AddListener(this);
        std::cout << "- DataStream Subscribed" << std::endl;
        std::cout << "  - Publication Id: " << publication->Id() << std::endl;
        std::cout << "  - Subscription Id: " << subscription->Id() << std::endl;
    }
    return true;
}

void HandsOnRoom::SubscribeAllDataStream() {
    for (auto& publication : p2proom_->Publications()) {
        this->SubscribeDataStream(std::move(publication));
    }
}

// [E] 指定のPublicationをSubscribeしているかチェックします。
bool HandsOnRoom::IsSubscribed(skyway::room::interface::RoomPublication* publication) {
    auto subscriptions = publication->Subscriptions();
    auto find =
        std::find_if(subscriptions.begin(),
                     subscriptions.end(),
                     [&](std::unique_ptr<skyway::room::interface::RoomSubscription>& subscription) {
                         return subscription->Subscriber()->Id() == room_member_->Id();
                     });
    return find != subscriptions.end();
}

// [F] Roomイベントのリスナを実装します。
// Room内の誰かがPublishするとこのイベントが発火します。
void HandsOnRoom::OnStreamPublished(
    std::unique_ptr<skyway::room::interface::RoomPublication> publication) {
    if (is_notify_) {
        std::cout << "<!-- [Event] StreamPublished: Id " << publication->Id() << "-->" << std::endl;
    }

    // イベントリスナーからSkyWayの操作を行う場合は別スレッドで実行する必要があります。
    auto subscribe_thread = std::make_unique<std::thread>(
        [this, threads_publication = std::move(publication)]() mutable {
            this->SubscribeDataStream(std::move(threads_publication));
        });
    threads_.emplace_back(std::move(subscribe_thread));
}

// [G] DataStreamのイベントのリスナを実装します。
// 文字列を受信すると発火するイベントです。
void HandsOnRoom::OnData(const std::string& data) {
    // 送られてくるDataの内容に合わせてGPIOの出力を変更します。
    if(data == "HIGH"){
        if (is_notify_) {
            std::cout << "<!-- [Event] Change GPIO to HIGH-->" << std::endl;
        }
        gpiod_line_set_value(gpio_line_out_, 1);
    }
    else if(data == "LOW"){
        if (is_notify_) {
            std::cout << "<!-- [Event] Change GPIO to LOW-->" << std::endl;
        }
        gpiod_line_set_value(gpio_line_out_, 0);
    }
    else{
        if (is_notify_) {
            std::cout << "<!-- [Event] Unknown Command: [" << data << "]-->" << std::endl;
        }
    }
}
// バイナリデータを受信すると発火するイベントです。
// 今回は使用しません。
void HandsOnRoom::OnDataBuffer(const uint8_t* data, size_t length) {};

// [H] Roomから退出します。
bool HandsOnRoom::LeaveRoom() {
    if (!room_member_) {
        return false;
    }
    is_leaving_ = true;
    // P2PRoomに紐づくイベントリスナの登録を解除します。
    if (p2proom_) {
        p2proom_->RemoveEventListener(this);
    }
    // 各threadの終了を待ちます。
    for (auto& th : threads_) {
        if (th && th->joinable()) {
            th->join();
        }
    }
    is_leaving_ = false;
    return room_member_->Leave();
}

// [I] リソースを破棄します。
void HandsOnRoom::Dispose() { 
    skyway::Context::Dispose(); 
    gpiod_chip_close(gpio_chip_);
}

商標

Linux®︎は、米国およびその他の国における Linus Torvalds の登録商標です。

いいなと思ったら応援しよう!