https://d226lax1qjow5r.cloudfront.net/blog/blogposts/build-social-media-style-stories-with-android-and-python/stories_videoapi_1200x600.png

AndroidとPythonでソーシャルメディア・スタイルのストーリーを構築する

最終更新日 March 23, 2021

所要時間:9 分

インスタグラムには、Snapchatにインスパイアされたストーリーズと呼ばれる人気機能がある。ストーリーズは、ユーザーが短期間(24時間)で消える動画や静止画を作成できる。その後、リンクトインやツイッターなど、他のソーシャルメディア・プラットフォームもこの機能を開発した。

このチュートリアルでは、VonageのVideo APIを使ってAndroid用のこの機能を構築します。また、クライアントのセッションとトークンを処理するサーバーも必要で、これはPythonを使って構築します。

前提条件

サーバーの構築

はじめに、モバイル・アプリケーションが通信できるサーバーを構築します。このサーバは Djangoを使います。

依存関係のインストール

まず、以下の2つのコマンドを実行してPython仮想環境を作成する:

python3 -m venv .venv source .venv/bin/activate

次に、バックエンドサーバーの依存関係をインストールする:

(.venv) $ pip install Django opentok

上のコマンドはDjangoをインストールし、Vonage Video Python SDKをOpentokします。

プロジェクトとアプリケーションの作成

以下のコマンドを実行して Django プロジェクトを初期化します:

(.venv) $ django-admin startproject storiesserver

コードを書く前に、Django プロジェクトの中に少なくとも一つのアプリケーションを作 る必要があります。Django プロジェクトは多くのアプリケーションで構成できます。例えば、プロジェクト A は Stories アプリケーションで、プロジェクト B はメッセージングアプリケーションや e-commerce アプリケーションにすることができます。まず、カレントディレクトリをプロジェクトディレクトリに変更し、下に示す 2 つのコマンドで新しいアプリケーションを初期化します:

(.venv) $ cd storiesserver (.venv) $ python manage.py startapp storiesapp

このチュートリアルではデータベースを使用しないので、次のコマンドを実行する必要はありません。しかし、マイグレーション・コマンドを実行しない限り、警告は続くので、以下のコマンドを実行して警告を止めてください。

(.venv) $ python manage.py migrate

Django ウェブアプリケーションが動作することを確認するには、ウェブサーバースクリプトを実行します。

(.venv) $ python manage.py runserver

ポート8000でlocalhostのウェブアプリケーションにアクセスできます。ブラウザでは次のように表示されます http://localhost:8000.このURLにアクセスすると、成功のメッセージとロケットの絵が表示されます。

環境変数

私たちのコードでは、Vonage Video(OpenTokとしても知られています)の API キーとシークレットを使用する必要があります。アプリケーションのセキュリティを高めるために、環境変数を使うことができます。ターミナルで、以下の例のように2つの値を設定します。必ず xxxxxxに置き換えてください。API キーと API シークレットの値は Video API ドキュメント.

(.venv) $ export OPENTOK_API_SECRET=xxxxx (.venv) $ export OPENTOK_API_KEY=xxxxx

ビューの作成

ビューは Django アプリケーションでレンダリングされるテンプレートです。この storiesserver/storiesapp/views.pyファイルを開き、その中身を以下のように置き換えてください:

from django.http import HttpResponse, JsonResponse

import os, time

from opentok import OpenTok, MediaModes, ArchiveModes

api_key = os.environ["OPENTOK_API_KEY"]
api_secret = os.environ["OPENTOK_API_SECRET"]
opentok = OpenTok(api_key, api_secret)

videos = [
]

index = 1

def get_token(request):
	global opentok
	session = opentok.create_session(media_mode=MediaModes.routed, archive_mode=ArchiveModes.manual)
	token = session.generate_token(expire_time=int(time.time()) + 200)
	return JsonResponse({"token": token, "session": session.session_id,
                     	"api_key": api_key})

def video_stream(request, archive_id):
	global opentok
	video = opentok.get_archive(archive_id)
	return HttpResponse(video.url)

def videos_list(request):
	global videos
	return JsonResponse(videos, safe=False)

def video_start_archive(request, session_id):
	global index, videos
	name = f"Story {index}"
	archive = opentok.start_archive(session_id)
	index += 1
	videos.append({
    	"name": name,
    	"archive_id": archive.id
	})
	return HttpResponse(f"{archive.id}")

def video_stop_archive(request, archive_id):
	global opentok
	opentok.stop_archive(archive_id)
	return HttpResponse("Stop Archiving")

def homepage(request):
	return HttpResponse("Hello")

このコードの最初の部分では、OpenTokインスタンスを作成し、APIキーとAPIシークレットに事前に設定した環境変数を使用します。

次に、2つのグローバル変数がある、 videosindex.これらは、ストーリー(またはビデオ)の情報を保持するメモリ上のミニデータベースのようなものです。

最初のメソッドでは、セッションとトークンを get_tokenメソッドでセッションとトークンを生成します。セッションを作成するには create_sessionメソッドを使います。このメソッドには、メディア・モードとアーカイブ・モードの2つのパラメータがあります。メディアモードには MediaModes.routedというのは、Video を Story として公開するからです。 MediaModes.routedつまり、ビデオを他のクライアントではなくサーバーに送信します。アーカイブモードには ArchiveModes.manualアーカイブ ID を取得できるように、手動で Video をアーカイブ(録画)するためです。このidは、録画ビデオを取得する際に重要である。アーカイブIDがない場合は、アーカイブ・リストから繰り返し取得する必要があり、不便です。トークンを生成するには generate_tokenメソッドを使います。このメソッドでは、期限切れの時刻をパラメータとして渡します。そして最後に、セッション、トークン、APIキーをクライアントに送信する。

2番目のメソッド video_streamメソッドでは、アーカイブ ID から Video URL を取得します。お気づきのように、このメソッドには通常のパラメータ以外に追加のパラメータがあります、 request.アーカイブIDをどのように取得するかは、別のメソッドで行います。

第三の方法である videos_listメソッドでは、動画のリストをクライアントに送信します。これは基本的に videos変数を JSON でラップしたものです。

第4の方法は video_start_archiveメソッドでは、Videoをアーカイブする。このメソッドには追加のパラメータがあります、 session_id.メソッドを使用します。 start_archiveメソッドを使います。OpenTok セッションを受け取ります。このメソッドを実行すると、Video セッションが録画されます。この start_archiveメソッドはアーカイブ ID を返します。これを videos変数に格納します。アーカイブ ID 以外に、この Video に "Story 1"、"Story 2" などの素敵な名前を付けます。

5つ目の方法である video_stop_archiveメソッドでは、Videoのアーカイブを停止する。このメソッドには追加のパラメーターがあります、 archive_id.つまり、Videoの録画を停止する。この処理を行うために stop_archiveメソッドを使います。パラメータとしてアーカイブ ID を受け取ります。

最後のメソッド homepageメソッドは、 Django Web アプリケーションにアクセスできるかどうかを検証するためのテスト用です。

URLマッピングの作成

次に、URLからこれらのビュー・メソッドへのマッピングを作成する必要がある。以下のファイルを作成し storiesserver/storiesapp/urls.pyファイルを作成し、以下の例をファイルにコピーします:

from django.urls import path

from . import views

urlpatterns = [
	path('', views.homepage, name='index'),
	path('token', views.get_token, name='token'),
	path('videos/<str:archive_id>', views.video_stream, name='videostream'),
	path('videos-list', views.videos_list, name='videoslist'),
	path('video-start-archive/<str:session_id>', views.video_start_archive, name='startarchive'),
	path('video-stop-archive/<str:archive_id>', views.video_stop_archive, name='stoparchive'),
]

この pathメソッドは3つの引数を受け付ける:

URL パス アクセスするメソッド 参照のためのルートの名前。

例えば、2番目のパスは tokenURLをビューの get_tokenメソッドにマップします。

Django アプリケーションのサーバ側で、ストーリーの URL にマップするために urls.pyファイルを変更する必要があります: storiesserver/storiesserver/urls.pyを探し、このファイルの内容を

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
	path('stories/', include('storiesapp.urls')),
]

このファイルでは、アプリケーションのURLファイルを stories/URLにマッピングします。例えば tokenURLでは、次のような完全なURLになります: http://localhost:8000/stories/token.

ングロク

開発中に Django アプリケーションをインターネットや Android デバイスに公開するために、 Ngrok を使います。アカウントに接続したら、Django ウェブアプリケーションのポート 8000 に転送する HTTP トンネルを開始します:

$ ./ngrok http 8000

トンネルが作成されると、以下のような公開URLが表示されます: https://xxxxxxxx.ngrok.io.次に storiesserver/storiesserver/settings.pyファイルに "xxxxxxxx.ngrok.io", "10.0.2.2""localhost"文字列を ALLOWED_HOSTS変数に追加する。アドレスは 10.0.2.2アドレスは、Androidエミュレータがあなたのコンピュータのlocalhostにアクセスする方法です。

あなたの ALLOWED_HOSTS変数はこのようになるはずだ:

ALLOWED_HOSTS = ["10.0.2.2", "localhost", "xxxxxxxx.ngrok.io"]

次に tokenURLにアクセスするには、このURLにアクセスする: https://xxxxxxxx.ngrok.io/stories/token.

この時点で、トークンの生成、ビデオ録画のアーカイブ、ビデオ録画URLの配信を行うサーバー・コンポーネントを構築しました。次に、このサーバーアプリケーションのクライアントとして動作するAndroidアプリケーションを構築します。このモバイルアプリケーションで、ビデオを録画し、録画したビデオを見ることができます。

クライアント側

Android Studioを起動し、新規プロジェクトを作成し、プロジェクト・テンプレートに "Empty Activity "を選択します。最小SDKには "API 16: Android 4.1 (Jelly Bean) "を、言語には "Kotlin "を選択します。

依存関係

Android Studioでプロジェクトをビルドする際に最初に必要なのは、必要な依存関係を追加することだ。プロジェクト・レベルの build.gradleプロジェクト・レベルの repositoriesブロックの中に次の行を追加します。 allprojectsブロックの中に次の行を追加します。それからファイルを同期する。

maven { url 'https://tokbox.bintray.com/maven' }

2つ目にしなければならないのは、同じ名前のファイルに依存関係を追加することだ。 build.gradleしかし、アプリケーション・レベル(またはモジュール・レベル)である。これらの行を dependenciesブロックの中に追加します。ファイルの同期を忘れないでください。

implementation 'com.opentok.android:opentok-android-sdk:2.19.+'
implementation "com.squareup.okhttp3:okhttp:4.2.1"
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'pub.devrel:easypermissions:3.0.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'

このプロジェクトでは、さらに5つのライブラリを使用する:

Vonage Video SDK (OpenTok) バックエンドサーバーにリクエストを送信するためのOkHttpライブラリ、JSONレスポンスを解析するためのGsonライブラリ、Androidアプリケーションがカメラとマイクの使用許可を求める際のパーミッションを処理するためのEasyPermissionsライブラリ、ノンブロッキングとブロッキングコードを便利に処理するためのCoroutinesライブラリ。

ネットワーク・セキュリティ設定

このステップはオプションです。アプリケーションをエミュレータでテストし、Ngrok を使いたくない場合は、ネットワーク設定ファイルを作成する必要があります。このファイルを xmlディレクトリの中に resディレクトリの中に network_security_config.xmlファイルを作成します。 xmlディレクトリを作成します。その中に以下をコピーする:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
	<domain-config cleartextTrafficPermitted="true">
    	<domain includeSubdomains="true">10.0.2.2</domain>
	</domain-config>
</network-security-config>

つまり、バックエンドのURLにHTTPSプロトコルを使用しなくてもアプリケーションを開発できるということです。

アンドロイド・マニフェスト

このネットワーク・コンフィギュレーションを有効にするには AndroidManifest.xmlファイルを開き、次の属性を applicationノードに追加します:

android:networkSecurityConfig="@xml/network_security_config"

次に manifestノードの中に

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

モバイル・アプリケーションをバックエンド・サーバーに接続したいので、これら2つの追加が必要なのだ。

このファイルにいる間に、後で作成される追加の2つのアクティビティへの参照を作成しましょう。唯一の activityノードの後に以下の行を追加します。

<activity android:name=".ViewingStoryActivity">
</activity>
<activity android:name=".CreatingStoryActivity">
</activity>

レイアウト

アクティビティは3つありますが、これらのアクティビティ用に3つのレイアウトを作成する必要があります。また、MainActivity内で使用するRecyclerViewの行レイアウト用に、追加のレイアウトを作成する必要があります。

まず、行レイアウトを作成します。 row.xmlディレクトリ内に app/src/main/res/layoutディレクトリ内にこの新しいファイルに以下のXMLをコピーしてください:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:id="@+id/row"
	android:orientation="horizontal"
	android:layout_width="match_parent"
	android:layout_height="wrap_content">
	<Button
    	android:text="TextView"
    	android:layout_width="wrap_content"
    	android:layout_height="48dp"
    	android:layout_margin="16dp"
    	android:id="@+id/buttonView"
    	android:layout_weight="1"/>
</LinearLayout>

上記は、ストーリーやビデオを見るためのアクティビティを起動するためのボタンの作成を定義しています。

次に、Videoを表示するレイアウトを作成します。レイアウト・ディレクトリに activity_viewing_story.xmlという名前の新しいファイルを作成し、以下のXMLをそのファイルに追加します:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
	xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
	android:layout_height="match_parent">

	<WebView
    	android:id="@+id/webview"
    	android:layout_width="match_parent"
    	android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

レイアウトには WebViewが含まれており、これがVideoをレンダリングします。

次に activity_creating_story.xmlファイルを作成しなければなりません。これはActivityがVideoを作成する際に使用するレイアウトです。ファイルの内容を削除し、次のコードを追加します:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:tools="http://schemas.android.com/tools"
	android:layout_width="match_parent"
	android:layout_height="match_parent">
	<FrameLayout
    	android:layout_width="match_parent"
    	android:layout_height="match_parent"
    	tools:ignore="MissingConstraints"
    	>
    	<FrameLayout
        	android:layout_width="match_parent"
        	android:layout_height="match_parent"
        	android:layout_marginBottom="120dp"
        	android:id="@+id/publisher"
        	/>
    	<Button
        	android:layout_margin="48dp"
        	android:layout_gravity="bottom|end"
        	android:id="@+id/publishbutton"
        	android:text="Upload Story"
        	android:layout_height="wrap_content"
        	android:layout_width="wrap_content"
        	tools:ignore="MissingConstraints" />
	</FrameLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

IDを持つFrameLayoutは、Video SDKによって使用されます。 publisherIDを持つFrameLayoutは、Video SDKによって、Videoサーバーに送信されるビデオを表示するために使用されます。このアクティビティのボタンの目的は、Videoの録画を停止してアクティビティを終了することです。

最後に activity_main.xmlファイルを編集する必要があります。このファイルには、MainActivityが使用するレイアウトが含まれており、作成されたすべてのストーリー(Video)を一覧表示します。このファイルには、ビデオを作成するためのアクティビティを起動するボタンも含まれます。このファイルの内容を以下のXMLに置き換えます:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:app="http://schemas.android.com/apk/res-auto"
	xmlns:tools="http://schemas.android.com/tools"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	tools:context=".MainActivity">

	<androidx.recyclerview.widget.RecyclerView
    	android:id="@+id/listview"
    	android:layout_width="match_parent"
    	android:layout_height="match_parent"
    	tools:layout_editor_absoluteX="0dp"
    	tools:layout_editor_absoluteY="89dp" />

	<com.google.android.material.floatingactionbutton.FloatingActionButton
    	android:id="@+id/fab"
    	android:layout_width="wrap_content"
    	android:layout_height="wrap_content"
    	android:clickable="true"
    	android:contentDescription="Plus"
    	app:layout_constraintBottom_toBottomOf="parent"
    	app:layout_constraintEnd_toEndOf="parent"
    	android:layout_marginBottom="48dp"
    	android:layout_marginEnd="48dp"
    	android:layout_marginRight="48dp"
    	/>
</androidx.constraintlayout.widget.ConstraintLayout>

サーバーURLの追加

サーバーのURLを strings.xmlディレクトリにある valuesディレクトリにあります。そのためには resourcesノードに追加します:

<string name="SERVER">http://localhost:8000</string>

ただし、Ngrokを使用している場合は、次の例に変更してください。 xxxxxをあなたのngrok URLに置き換えてください):

<string name="SERVER">https://xxxxxx.ngrok.io</string>

クラスとアクティビティ

まず、RecyclerViewに必要なクラスを作成する必要があります。RecyclerViewにはアダプターとホルダーが必要です。ホルダー・ファイルは StoryViewHolder.ktという名前のファイルを packageディレクトリに作成します。パッケージは com.example.storiesapplicationのようなものになります。 javaディレクトリ内にあるようなものになります。この新しいファイルに以下のコードをコピーする:

package com.example.storiesapplication

import android.view.View
import android.widget.Button
import androidx.recyclerview.widget.RecyclerView


class StoryViewHolder(private val view : View, onClick: (view: View) -> Unit) : RecyclerView.ViewHolder(view) {

	private val buttonView : Button = this.view.findViewById(R.id.buttonView)

	init {
    	buttonView.setOnClickListener(onClick)
	}

	fun bindModel(item : String) {
    	this.buttonView.text = item
	}

}

作成するクラスやアクティビティ・ファイルごとに、パッケージが異なる場合は、必ず com.example.storiesapplicationパッケージが異なる場合は、パッケージを自分のパッケージに変更してください。

これは標準的なホルダークラスで、文字列パラメータでボタンのテキストを設定し、ボタンのコールバックを設定します。

ホルダー・クラスを作成した後、アダプター・クラスが必要です。アダプターはデータとRecyclerViewのビューの橋渡しです。新しいファイルを java/package/ディレクトリに StoryAdapter.ktファイルを作成し、このファイルに以下のコードを追加します:

package com.example.storiesapplication

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView


class StoryAdapter(private val dataset: Array<VideoJson>, val onClick: (view: View) -> Unit) : RecyclerView.Adapter<StoryViewHolder>() {

	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : StoryViewHolder {
    	val linearLayout = LayoutInflater.from(parent.context).inflate(R.layout.row, parent, false)
    	return StoryViewHolder(linearLayout, onClick)
	}

	override fun onBindViewHolder(holder: StoryViewHolder, position: Int) {
    	holder.bindModel(dataset[position].name)
	}

	override fun getItemCount() = dataset.size
}

この onCreateViewHolderメソッドは rowレイアウトを膨張させ、ホルダー・クラスのインスタンスを作成します。この onBindViewHolderメソッドはデータをRecyclerViewの特定の行に設定し getItemCountメソッドは格納されているアイテムの数を返します。

いよいよMainActivityにRecyclerViewを作成します。ファイルを修正します。 MainActivity.ktファイルの内容を以下のように書き換えてください:

package com.example.storiesapplication

import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.gson.Gson
import com.opentok.android.*
import okhttp3.*
import kotlinx.coroutines.*

const val REQUEST_CODE_CREATE_STORY = 1
const val REQUEST_CODE_VIEW_STORY = 2


class MainActivity : AppCompatActivity() {

	private lateinit var recyclerView: RecyclerView
	private lateinit var viewAdapter: RecyclerView.Adapter<*>
	private lateinit var viewManager: RecyclerView.LayoutManager

	private val client = OkHttpClient()

	private var videosMap = mutableMapOf<String, String>()

	override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
    	setContentView(R.layout.activity_main)

    	val fab: View = findViewById(R.id.fab)
    	fab.setOnClickListener { view ->
        	val self = this
        	CoroutineScope(Dispatchers.IO).launch {
            	val deferredToken = async { getToken() }
            	val results = deferredToken.await()

            	withContext(Dispatchers.Main) {
                	val intent = Intent(self, CreatingStoryActivity::class.java).apply {
                    	putExtra("token", results)
                	}
                	startActivityForResult(intent, REQUEST_CODE_CREATE_STORY)
            	}
        	}
    	}

    	loadUpVideos()

	}

	fun loadUpVideos() {
    	val self = this
    	viewManager = LinearLayoutManager(this)
    	CoroutineScope(Dispatchers.IO).launch {
        	val deferredVideos = async { getVideos() }
        	val videosList = deferredVideos.await()
        	videosMap = mutableMapOf<String, String>()
        	for (video in videosList) {
            	videosMap.put(video.name, video.archive_id)
        	}
        	viewAdapter = StoryAdapter(videosList) { view: View ->
            	val button: Button = view as Button
            	CoroutineScope(Dispatchers.IO).launch {
                	val archiveId = videosMap[button.text.toString()]
                	withContext(Dispatchers.Main) {
                    	val intent = Intent(self, ViewingStoryActivity::class.java).apply {
                        	putExtra("archive_id", archiveId)
                    	}
                    	startActivityForResult(intent, REQUEST_CODE_VIEW_STORY)
                	}
            	}
        	}
        	withContext(Dispatchers.Main) {
            	recyclerView = findViewById<RecyclerView>(R.id.listview).apply {
                	setHasFixedSize(true)
                	layoutManager = viewManager
                	adapter = viewAdapter
            	}
        	}
    	}
	}

	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    	super.onActivityResult(requestCode, resultCode, data)

    	if (requestCode== REQUEST_CODE_CREATE_STORY) {
        	loadUpVideos()
    	}
	}

	suspend fun getToken(): Array<String> {
    	var request = Request.Builder().url("${getString(R.string.SERVER)}/stories/token").build()
    	client.newCall(request).execute().use { response ->
        	val string = response.body!!.string()
        	val gson = Gson()
        	val tokenJson = gson.fromJson(string, TokenJson::class.java)
        	val session_id = tokenJson.session
        	val token = tokenJson.token
        	val api_key = tokenJson.api_key
        	return arrayOf<String>(api_key, token, session_id)
    	}
	}

	suspend fun getVideos(): Array<VideoJson> {
    	var request = Request.Builder().url("${getString(R.string.SERVER)}/stories/videos-list").build()
    	client.newCall(request).execute().use { response ->
        	val string = response.body!!.string()
        	val gson = Gson()
        	val videosJson = gson.fromJson(string, Array<VideoJson>::class.java)
        	return videosJson
    	}
	}

}

class TokenJson(
	val token: String,
	val session: String,
	val api_key: String
)

class VideoJson(
	val name: String,
	val archive_id: String
)

この onCreateメソッドは、このアクティビティのフローティングボタンにコールバックを追加します。 getTokenメソッドを呼び出してトークン、セッション、APIキーを取得し、それらを CreatingStoryActivityアクティビティに渡します。RecyclerView は Video リストも読み込みます。 ViewingStoryActivityメソッドを呼び出すコールバックを追加します。

次に CreatingStoryActivity.ktファイルを app/src/main/java/com/example/storiesapplicationファイルを作成する必要があります。 MainActivity.ktファイルがあるのと同じディレクトリ) にファイルを作成します。この新しいファイルに以下のコードをコピーします:

package com.example.storiesapplication

import android.Manifest
import android.opengl.GLSurfaceView
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import com.opentok.android.*
import kotlinx.coroutines.*
import okhttp3.OkHttpClient
import okhttp3.Request
import pub.devrel.easypermissions.AfterPermissionGranted
import pub.devrel.easypermissions.EasyPermissions


class CreatingStoryActivity : AppCompatActivity(), Session.SessionListener, PublisherKit.PublisherListener  {

	private var mPublisherViewContainer: FrameLayout? = null
	private var mPublisher: Publisher? = null

	private val client = OkHttpClient()

	companion object {
    	private val LOG_TAG = "android-stories"
    	const val RC_VIDEO_APP_PERM = 124
    	private var mSession: Session? = null
	}

	private var token: String? = null
	private var apiKey: String? = null
	private var sessionId: String? = null
	private var archiveId: String? = null

	override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
    	setContentView(R.layout.activity_creating_story)

    	val message = intent.getStringArrayExtra("token")
    	message?.let {
        	apiKey = it[0]
        	token = it[1]
        	sessionId = it[2]

        	requestPermissions()
    	}

    	val button = findViewById<Button>(R.id.publishbutton)
    	button.setOnClickListener {
        	mSession!!.unpublish(mPublisher)
        	CoroutineScope(Dispatchers.IO).launch {
            	val deferredStopArchive = async { stopArchive() }
            	deferredStopArchive.await()
            	withContext(Dispatchers.Main) {
                	finish()
            	}
        	}
    	}
	}

	override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
    	super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    	EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
	}

	@AfterPermissionGranted(RC_VIDEO_APP_PERM)
	private fun requestPermissions() {
    	val perms = arrayOf<String>(Manifest.permission.INTERNET, Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
    	if (EasyPermissions.hasPermissions(this, *perms)) {
        	mPublisherViewContainer = findViewById(R.id.publisher)

        	mSession = Session.Builder(this, this.apiKey, this.sessionId).build()
        	mSession?.let {
            	it.setSessionListener(this)
            	it.connect(this.token)
        	}

    	} else {
        	EasyPermissions.requestPermissions(this, "This app needs access to your camera and mic to make video calls", RC_VIDEO_APP_PERM, *perms)
    	}
	}

	suspend fun startArchive(): Unit {
    	var request = Request.Builder().url("${getString(R.string.SERVER)}/stories/video-start-archive/${sessionId}").build()
    	client.newCall(request).execute().use { response ->
        	val string = response.body!!.string()
        	archiveId = string
    	}
	}

	suspend fun stopArchive(): Unit {
    	var request = Request.Builder().url("${getString(R.string.SERVER)}/stories/video-stop-archive/${archiveId}").build()
    	client.newCall(request).execute()
	}

	override fun onConnected(session: Session?) {
    	Log.i(LOG_TAG, "Session Connected")

    	mPublisher = Publisher.Builder(this).build()
    	mPublisher?.let {
        	it.setPublisherListener(this)
            it.renderer.setStyle(BaseVideoRenderer.STYLE_VIDEO_SCALE, BaseVideoRenderer.STYLE_VIDEO_FILL)

        	mPublisherViewContainer!!.addView(it.view)

        	if (it.view is GLSurfaceView) {
            	(it.view as GLSurfaceView).setZOrderOnTop(true)
        	}

        	mSession!!.publish(mPublisher)

        	CoroutineScope(Dispatchers.IO).launch {
            	val deferredStartArchive = async { startArchive() }
            	deferredStartArchive.await()
        	}
    	}

	}

	override fun onDisconnected(session: Session?) {
    	Log.i(LOG_TAG, "Session Disconnected")
	}

	override fun onStreamReceived(session: Session?, stream: Stream?) {
    	Log.i(LOG_TAG, "Stream Received")
	}

	override fun onStreamDropped(session: Session?, stream: Stream?) {
    	Log.i(LOG_TAG, "Stream Dropped")
	}

	override fun onError(publisherKit: Session?, opentokError: OpentokError?) {
    	opentokError?.let {
        	Log.e(LOG_TAG, "Session error: " + opentokError.getMessage())
    	}
	}

	override fun onError(publisherKit: PublisherKit?, opentokError: OpentokError?) {
    	opentokError?.let {
        	Log.e(LOG_TAG, "Publisher error: " + opentokError.getMessage())
    	}
	}

	override fun onStreamCreated(publisherKit: PublisherKit?, stream: Stream?) {
    	Log.i(LOG_TAG, "Publisher onStreamCreated")
	}

	override fun onStreamDestroyed(publisherKit: PublisherKit?, stream: Stream?) {
    	Log.i(LOG_TAG, "Publisher onStreamDestroyed")
	}
}

上記の方法では onCreateメソッドで、カメラ、インターネット、音声録音の許可を要求する。パーミッションがあれば、トークン、セッション、APIキーを使ってセッション・オブジェクトを作成する。また、このセッション・オブジェクトのリスナーを設定しなければならない。また、このActivityのボタンにコールバックを設定します。このボタンは、ビデオのアーカイブを停止するリクエストを送信します。

セッションリスナーが必要とするメソッドはたくさんある。最も重要なのは onConnectedメソッドで、セッションが OpenTok サーバーに接続したときに呼び出されます。

パブリッシャーオブジェクトを作成し、FrameLayoutをパブリッシャーのビューとして設定します。また、セッションオブジェクトをこのパブリッシャーオブジェクトに接続します。そして、Videoのアーカイブを開始するリクエストオブジェクトを作成します。

次に ViewingStoryActivity.ktファイルを app/src/main/java/com/example/storiesapplicationファイルを作成する必要があります。 MainActivity.ktファイルがあるのと同じディレクトリ)にファイルを作成する必要がある:

package com.example.storiesapplication

import android.os.Bundle
import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*
import okhttp3.OkHttpClient
import okhttp3.Request


class ViewingStoryActivity : AppCompatActivity() {

	private val client = OkHttpClient()

	override fun onCreate(savedInstanceState: Bundle?) {
    	super.onCreate(savedInstanceState)
    	setContentView(R.layout.activity_viewing_story)

    	val webView: WebView = findViewById(R.id.webview)
    	val message = intent.getStringExtra("archive_id")
    	message?.let {
        	CoroutineScope(Dispatchers.IO).launch {
            	val deferredVideoUrl = async { getVideoUrl(it) }
            	val videoUrl = deferredVideoUrl.await()
            	withContext(Dispatchers.Main) {
                	webView.loadUrl(videoUrl)
            	}
        	}
    	}
	}

	suspend fun getVideoUrl(archiveId: String): String {
    	var request = Request.Builder().url("${getString(R.string.SERVER)}/stories/videos/${archiveId}").build()
    	client.newCall(request).execute().use { response ->
        	return response.body!!.string()
    	}
	}
}

まず、Django アプリケーションにリクエストを送ることで、アーカイブ ID から Video の URL を取得します。そして、その URL を WebView に読み込みます。

アプリケーションの起動

アプリケーションを起動する。Androidデバイスを使いたい場合は、Ngrokを使うか、クラウド上にDjangoアプリケーションをデプロイすることを忘れないでください。最初、空の画面とフローティングボタンが表示されます。

Main Android screen with floating button

フローティングボタンを押すと、デバイスがカメラからOpenTokサーバーにビデオをストリーミングします。ストーリーを十分に録画したら、画面の "ストーリーをアップロード "ボタンを押してください。

Android screen displaying what's being captured with your camera

メイン画面に戻り、ストーリーのリストが表示されます。

Main Android screen now displaying your stories

ストーリーの行を押すと、ストーリーの視聴画面に移動します。以前に録画したビデオを見ることができます。

Screen displaying story being played back to you.

結論

このアプリケーションは完璧とは言い難い。お気づきのように、録画されたビデオは録画セッションよりも短い。これは、Videoをアーカイブするリクエストを送るのに時間がかかるからだ。その上、ストーリーリストは単純なRecyclerViewです。横スクロールに変換することができます。各ストーリーは、Instagram Storiesがそうであるように、円形に包まれている!また、認証や認可もありません。ストーリーのオーナーはいません。この Video は OpenTok サーバーにしばらく保存されます。その後、ビデオは削除されます。その前にビデオを別の場所に保存することもできます。

リソース

  • Vonage Video APIのドキュメントをご覧ください。 こちら

  • このブログ記事のコードは GitHub

シェア:

https://a.storyblok.com/f/270183/400x600/b5c7e9f07d/arjuna-sky-kok.png
Arjuna Sky Kok

Arjuna Sky Kok is the author of "Hands-on Blockchain for Python Developers" and the creator of PredictSalary, a tool to predict the salary ranges from job opportunities. He lives in Jakarta, Indonesia