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

Erstellen von Geschichten im Social-Media-Stil mit Android und Python

Zuletzt aktualisiert am March 23, 2021

Lesedauer: 19 Minuten

Instagram hat eine beliebte Funktion namens Stories, die stark von Snapchat inspiriert ist. Mit Stories können Nutzer ein Video oder ein Foto erstellen, das nach kurzer Zeit (24 Stunden) wieder verschwindet. Später haben auch andere Social-Media-Plattformen diese Funktion entwickelt, z. B. Linkedin und Twitter.

In diesem Tutorial werden wir diese Funktion für Android mit der Video API von Vonage erstellen. Außerdem benötigen wir einen Server, um die Sitzungen und Token für den Client zu verwalten, der mit Python erstellt wird.

Voraussetzungen

Aufbau des Servers

Zu Beginn werden wir einen Server aufbauen, mit dem unsere mobile Anwendung kommunizieren kann. Dieser Server wird mit Djangoverwenden, ein Python-Web-Framework.

Installieren von Abhängigkeiten

Erstellen Sie zunächst eine virtuelle Python-Umgebung, indem Sie die folgenden beiden Befehle ausführen:

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

Dann können wir die Abhängigkeiten für unseren Backend-Server installieren:

(.venv) $ pip install Django opentok

Der obige Befehl installiert Django und Opentok, das Vonage Video Python SDK.

Erstellen eines Projekts und einer Anwendung

Initialisieren Sie Ihr Django-Projekt, indem Sie den folgenden Befehl ausführen:

(.venv) $ django-admin startproject storiesserver

Bevor wir mit dem Schreiben des Codes beginnen, müssen wir mindestens eine Anwendung innerhalb des Django-Projekts erstellen. Ein Django-Projekt kann aus mehreren Anwendungen bestehen. Projekt A kann zum Beispiel eine Stories-Anwendung sein, während Projekt B eine Messaging-Anwendung oder eine E-Commerce-Anwendung sein könnte. Wechseln Sie zunächst von Ihrem aktuellen Verzeichnis in das Projektverzeichnis und initialisieren Sie dann eine neue Anwendung mit den beiden unten aufgeführten Befehlen:

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

Sie brauchen den nächsten Befehl nicht auszuführen, da wir in diesem Tutorial keine Datenbank verwenden werden. Es wird jedoch eine Warnung angezeigt, wenn wir den Migrationsbefehl nicht ausführen. Führen Sie also den folgenden Befehl aus, um diese Warnungen zu beenden.

(.venv) $ python manage.py migrate

Um sicherzustellen, dass unsere Django-Webanwendung funktioniert, können Sie das Webserver-Skript ausführen.

(.venv) $ python manage.py runserver

Sie können auf Ihre Webanwendung unter localhost mit Port 8000 zugreifen. In Ihrem Browser sieht das dann so aus http://localhost:8000. Wenn Sie diese URL aufrufen, werden Sie mit einer Erfolgsmeldung und dem Bild einer Rakete begrüßt.

Umgebungsvariablen

Unser Code muss Ihren Vonage Video (auch bekannt als OpenTok) API-Schlüssel und -Geheimnis verwenden, die niemals fest in Ihrer Anwendung kodiert sein sollten. Um die Sicherheit der Anwendung zu erhöhen, können Sie Umgebungsvariablen verwenden. In Ihrem Terminal können Sie die beiden Werte wie in den Beispielen unten dargestellt setzen. Stellen Sie sicher, dass Sie xxxxxx durch Ihre Werte. Sie können diese Werte für den API-Schlüssel und das API-Geheimnis in den Vonage Video API-Dokumenten.

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

Erstellen Sie die Ansichten

Ansichten sind die Vorlagen, die in Ihrer Django-Anwendung gerendert werden. Öffnen Sie die storiesserver/storiesapp/views.py Datei und ersetzen Sie den Inhalt durch den folgenden Text:

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")

Der erste Teil dieses Codes erstellt eine OpenTok-Instanz, die unsere voreingestellten Umgebungsvariablen für den API-Schlüssel und das API-Geheimnis verwenden wird.

Dann haben wir zwei globale Variablen, videos und index. Sie sind wie eine Mini-Datenbank im Speicher, in der die Informationen zu unseren Geschichten (oder Videos) gespeichert sind.

In der ersten Methode erzeugen wir unsere Sitzung und unser Token mit der get_token Methode. Um eine Sitzung zu erstellen, verwenden wir die create_session Methode der OpenTok-Instanz. Sie akzeptiert zwei Parameter: einen Medienmodus und einen Archivmodus. Wir verwenden den MediaModes.routed Wert für den Medienmodus, weil wir ein Video als Story veröffentlichen, nicht für einen Videochat. MediaModes.routed bedeutet, dass wir das Video an den Server und nicht an einen anderen Client senden. Wir verwenden den ArchiveModes.manual Wert für den Archivierungsmodus, weil wir das Video manuell archivieren (aufzeichnen) wollen, um die Archiv-ID zu erhalten. Diese ID ist wichtig, wenn wir das aufgezeichnete Video abrufen wollen. Andernfalls müssen wir es aus der Archivliste abrufen, was umständlich ist. Um das Token zu erzeugen, verwenden wir die generate_token Methode der Sitzungsinstanz. Wir übergeben den abgelaufenen Zeitwert als Parameter in dieser Methode. Am Ende senden wir die Sitzung, das Token und den API-Schlüssel an den Client.

Mit der zweiten Methode, der video_stream Methode, wird die Video-URL aus der Archiv-ID ermittelt. Wie Sie sehen, hat diese Methode einen zusätzlichen Parameter neben dem üblichen Parameter, request. Wie wir die Archiv-ID erhalten, wird in einer anderen Methode durchgeführt.

Bei der dritten Methode, der videos_list Methode, senden wir die Liste unserer Videos an den Kunden. Es ist im Grunde unsere videos Variable in JSON eingewickelt.

Bei der vierten Methode, der video_start_archive Methode, wird das Video archiviert. Diese Methode hat einen zusätzlichen Parameter, session_id. Wir verwenden die start_archive Methode aus der OpenTok-Instanz. Sie nimmt eine OpenTok-Sitzung an. Nach dem Ausführen dieser Methode wird unsere Video-Sitzung aufgezeichnet. Die Methode start_archive Methode gibt die Archiv-ID zurück. Wir speichern sie in der videos Variable. Abgesehen von der Archiv-ID generieren wir einen schönen Namen für dieses Video, etwa "Story 1", "Story 2" und so weiter.

Bei der fünften Methode, der video_stop_archive Methode, wird die Archivierung des Videos beendet. Diese Methode hat einen zusätzlichen Parameter, archive_id. Mit anderen Worten, wir beenden die Aufzeichnung des Videos. Wir verwenden die stop_archive Methode der OpenTok-Instanz, um diese Aufgabe zu erledigen. Sie akzeptiert die Archiv-ID als Parameter.

Die letzte Methode, die homepage Methode, dient nur zu Testzwecken, um zu überprüfen, ob auf die Django-Webanwendung zugegriffen werden kann oder nicht.

URLs Mapping erstellen

Dann müssen wir eine Zuordnung von URLs zu den Methoden dieser Ansichten erstellen. Erstellen Sie die storiesserver/storiesapp/urls.py Datei und kopieren Sie das folgende Beispiel in die Datei:

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'),
]

Die Methode path Methode nimmt drei Argumente entgegen:

URL-Pfad Die Methode, auf die zugegriffen werden soll Ein Name für die Route zu Referenzzwecken.

Zum Beispiel bildet der zweite Pfad die token URL auf die get_token Methode in den Ansichten zu.

Auf der Server-Seite der Django-Anwendung müssen wir die urls.py Datei ändern, damit sie auf die URLs der Geschichten abgebildet wird: storiesserver/storiesserver/urls.py und ersetzen Sie den Inhalt dieser Datei durch:

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

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

In dieser Datei bilden wir die URL-Datei der Anwendung auf die stories/ URL AB. Zum Beispiel mit der token URL hätten wir die folgende vollständige URL: http://localhost:8000/stories/token.

Ngrok

Um die Django-Anwendung während der Entwicklung dem Internet und unseren Android-Geräten zugänglich zu machen, werden wir Ngrok verwenden. Nachdem Sie Ihren Account verbunden haben, können wir einen HTTP-Tunnel starten, der an den Port 8000 unserer Django-Webanwendung weitergeleitet wird:

$ ./ngrok http 8000

Wenn der Tunnel erstellt wurde, sehen Sie die öffentliche URL, die etwa so aussieht: https://xxxxxxxx.ngrok.io. Dann müssen Sie die storiesserver/storiesserver/settings.py Datei ändern, indem Sie die "xxxxxxxx.ngrok.io", "10.0.2.2" und "localhost" in die Variablen ALLOWED_HOSTS Variable hinzufügen. Die 10.0.2.2 Adresse ist die Art und Weise, wie der Android-Emulator auf localhost in Ihrem Computer zugreift.

Ihre ALLOWED_HOSTS Variable sollte nach der Änderung so aussehen:

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

Um dann auf die token URL aufzurufen, würden Sie diese URL aufrufen: https://xxxxxxxx.ngrok.io/stories/token.

Zu diesem Zeitpunkt haben Sie die Serverkomponente erstellt, die Token generieren, Videoaufzeichnungen archivieren und die URL für Videoaufzeichnungen bereitstellen kann. Als Nächstes werden Sie eine Android-Anwendung erstellen, die als Client für diese Serveranwendung fungiert. Mit dieser mobilen Anwendung können Sie Videos aufzeichnen und die Videoaufnahmen ansehen.

Kunden-Seite

Starten Sie Android Studio, erstellen Sie ein neues Projekt und wählen Sie "Empty Activity" als Projektvorlage. Wählen Sie für das Mindest-SDK "API 16: Android 4.1 (Jelly Bean)" und für die Sprache "Kotlin".

Abhängigkeiten

Wenn Sie ein Projekt in Android Studio erstellen, müssen Sie als Erstes alle benötigten Abhängigkeiten hinzufügen. Öffnen Sie die build.gradle in der Projektebene und fügen Sie die folgende Zeile innerhalb des repositories Block, der sich innerhalb des allprojects Blocks befindet. Dann synchronisieren Sie die Datei.

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

Der zweite Schritt ist das Hinzufügen von Abhängigkeiten zu der Datei, die den gleichen Namen hat build.gradle aber auf Anwendungsebene (oder Modulebene). Fügen Sie diese Zeilen innerhalb des dependencies Blocks ein. Vergessen Sie nicht, die Datei zu synchronisieren.

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'

Dieses Projekt verwendet fünf zusätzliche Bibliotheken, und zwar:

Vonage Video SDK (OpenTok) OkHttp-Bibliothek zum Senden von Anfragen an unseren Backend-Server, Gson-Bibliothek zum Parsen von JSON-Antworten, EasyPermissions-Bibliothek zur Handhabung von Berechtigungen, wenn unsere Android-Anwendung um Erlaubnis zur Verwendung von Kamera und Mikrofon bittet, Coroutines-Bibliothek zur bequemen Handhabung von nicht blockierendem und blockierendem Code.

Netzwerksicherheitskonfiguration

Dieser Schritt ist optional. Wenn Sie die Anwendung mit einem Emulator testen und nicht Ngrok verwenden möchten, müssen Sie eine Netzwerkkonfigurationsdatei erstellen. Erstellen Sie das xml Verzeichnis innerhalb des res und erstellen Sie dann eine neue Datei namens network_security_config.xml Datei innerhalb des Verzeichnisses xml Verzeichnis. Kopieren Sie das Folgende in diese Datei:

<?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>

Das bedeutet, dass Sie die Anwendung ohne das HTTPS-Protokoll für die Backend-URL entwickeln können.

Android-Manifest

Um diese Netzwerkkonfiguration zu aktivieren, öffnen Sie die Datei AndroidManifest.xml Datei und fügen Sie das folgende Attribut in application Knoten:

android:networkSecurityConfig="@xml/network_security_config"

Fügen Sie dann folgendes innerhalb des manifest Knotens ein.

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

Wir benötigen diese beiden Zusätze, weil wir unsere mobile Anwendung mit unserem Backend-Server verbinden wollen.

Wenn wir schon in dieser Datei sind, lassen Sie uns Referenzen zu zwei weiteren Aktivitäten erstellen, die später erstellt werden. Fügen Sie diese Zeilen nach dem einzigen activity Knoten ein.

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

Layout

Wir haben drei Activities, aber wir müssen 3 Layouts für diese Activities erstellen. Außerdem müssen wir ein zusätzliches Layout für das Zeilenlayout in einer RecyclerView erstellen, die innerhalb der MainActivity verwendet wird.

Erstellen Sie zunächst das Zeilenlayout, indem Sie eine neue Datei namens row.xml innerhalb des Verzeichnisses app/src/main/res/layout Verzeichnis. Kopieren Sie die folgende XML-Datei in diese neue Datei::

<?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>

Die obigen Angaben definieren die Erstellung einer Schaltfläche, die zum Starten einer Aktivität verwendet wird, um die Geschichte oder das Video anzusehen.

Als Nächstes müssen wir ein Layout erstellen, um ein Video anzuzeigen. Erstellen Sie eine neue Datei namens activity_viewing_story.xml im Layoutverzeichnis und fügen Sie der Datei die folgende XML-Datei hinzu:

<?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>

Das Layout enthält eine WebView das das Video wiedergibt.

Als nächstes müssen wir die activity_creating_story.xml Datei erstellen. Dies ist das Layout, das von der Aktivität zur Erstellung eines Videos verwendet wird. Löschen Sie den Inhalt der Datei und fügen Sie den folgenden Code hinzu:

<?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>

Das FrameLayout mit der publisher ID wird vom Video SDK verwendet, um das Video anzuzeigen, das an den Video-Server gesendet wird. Der Zweck der Schaltfläche in dieser Aktivität ist es, die Aufzeichnung des Videos zu stoppen und die Aktivität zu beenden.

Zum Schluss müssen wir die Datei activity_main.xml Datei bearbeiten. Diese Datei enthält das von der MainActivity verwendete Layout, in dem alle erstellten Geschichten (Videos) aufgelistet werden. Die Datei enthält auch eine Schaltfläche zum Starten der Aktivität, um ein Video zu erstellen. Ersetzen Sie den Inhalt dieser Datei durch die folgende XML-Datei:

<?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>

Hinzufügen der Server-URL

Definieren wir die Server-URL in der Datei strings.xml Datei, die sich im Verzeichnis values Verzeichnis zu finden ist. Fügen Sie dazu Folgendes zu Ihrem resources Knoten:

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

Wenn Sie jedoch Ngrok verwenden, ändern Sie es in das folgende Beispiel (stellen Sie sicher, dass Sie xxxxx durch Ihre Ngrok-URL ersetzen):

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

Unterricht und Aktivitäten

Zunächst müssen wir die notwendigen Klassen für eine RecyclerView erstellen. Ein RecyclerView benötigt einen Adapter und einen Halter. Erstellen Sie eine Halterdatei, indem Sie die Datei mit dem Namen StoryViewHolder.kt innerhalb des package Verzeichnis. Das Paket wird etwas ähnliches sein wie com.example.storiesapplication das sich innerhalb des Verzeichnisses java Verzeichnis befindet. Kopieren Sie den folgenden Code in diese neue Datei:

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
	}

}

Stellen Sie sicher, dass Sie für jede Klasse oder Activity-Datei, die Sie erstellen, das com.example.storiesapplication Paket in Ihr Paket, wenn Ihr Paket anders ist.

Dies ist eine Standard-Halterklasse, in der Sie den Text der Schaltfläche mit einem String-Parameter festlegen und einen Callback für die Schaltfläche einstellen.

Nachdem wir die Halterklasse erstellt haben, benötigen wir eine Adapterklasse. Ein Adapter ist eine Brücke zwischen Daten und der Ansicht von RecyclerView. Erstellen Sie eine neue Datei im java/package/ Verzeichnis mit dem Namen StoryAdapter.kt Datei und fügen Sie den folgenden Code in diese Datei ein:

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
}

Die Methode onCreateViewHolder Methode bläst das row Layout auf und erzeugt eine Instanz der Halterklasse. Die Methode onBindViewHolder Methode setzt die Daten auf eine bestimmte Zeile der RecyclerView und die getItemCount Methode gibt die Anzahl der gespeicherten Elemente zurück.

Nun ist es an der Zeit, die RecyclerView in der MainActivity zu erstellen. Ändern Sie die MainActivity.kt Datei, indem Sie den Inhalt durch den folgenden ersetzen:

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
)

Die Methode onCreate Methode fügt der schwebenden Schaltfläche in dieser Aktivität einen Callback hinzu, der die getToken Methode aufruft, um das Token, die Sitzung und den API-Schlüssel zu erhalten, bevor sie an die CreatingStoryActivity Aktivität weitergibt. Die RecyclerView lädt auch die Videoliste, und für jede Zeile wird ein Callback hinzugefügt, um die ViewingStoryActivity Aktivität.

Als nächstes müssen wir die Datei CreatingStoryActivity.kt Datei in dem app/src/main/java/com/example/storiesapplication (das gleiche Verzeichnis, in dem sich die MainActivity.kt Datei befindet), die die Aktivität ist, die ein Video auf dem OpenTok-Server veröffentlicht. Kopieren Sie den folgenden Code in diese neue Datei::

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")
	}
}

Bei der onCreate Methode fragen wir die Berechtigungen für die Kamera, das Internet und die Audioaufzeichnung ab. Wenn wir die Berechtigungen haben, erstellen wir ein Sitzungsobjekt mit dem Token, der Sitzung und dem API-Schlüssel. Wir müssen auch den Hörer für dieses Sitzungsobjekt festlegen. Außerdem legen wir einen Callback für die Schaltfläche in dieser Aktivität fest. Die Schaltfläche sendet eine Anfrage zum Beenden der Archivierung des Videos.

Wir haben viele Methoden, die der Sitzungshörer benötigt. Die wichtigste davon ist die onConnected Methode, die aufgerufen wird, wenn sich unsere Sitzung mit dem OpenTok-Server verbunden hat.

Wir erstellen ein Publisher-Objekt und legen das FrameLayout als Ansicht des Publishers fest. Außerdem müssen wir unser Sitzungsobjekt mit diesem Verlagsobjekt verbinden. Dann erstellen wir ein Anforderungsobjekt, um mit der Archivierung des Videos zu beginnen.

Dann müssen wir die ViewingStoryActivity.kt Datei in dem app/src/main/java/com/example/storiesapplication erstellen (das gleiche Verzeichnis, in dem sich die MainActivity.kt Datei befindet), die die Aktivität ist, die ein Video ansieht, also fügen Sie den folgenden Code in diese Datei ein::

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()
    	}
	}
}

Zunächst holen wir uns die Video-URL aus der Archiv-ID, indem wir eine Anfrage an unsere Django-Anwendung senden. Dann laden wir die URL in die WebView.

Starten der Anwendung

Starten Sie die Anwendung. Denken Sie daran, Ngrok zu verwenden oder die Django-Anwendung in der Cloud bereitzustellen, wenn Sie ein Android-Gerät verwenden möchten. Zu Beginn sehen Sie einen leeren Bildschirm und eine schwebende Schaltfläche.

Main Android screen with floating button

Drücken Sie die Taste "Floating", und das Gerät überträgt ein Video von Ihrer Kamera an den OpenTok-Server. Wenn Sie genug für Ihre Story aufgenommen haben, drücken Sie die Schaltfläche "Story hochladen" auf dem Bildschirm.

Android screen displaying what's being captured with your camera

Sie werden wieder zum Hauptbildschirm zurückgeleitet, der nun eine Liste Ihrer Beiträge anzeigt.

Main Android screen now displaying your stories

Wenn Sie auf die Beitragsreihe drücken, werden Sie zum Bildschirm "Beitrag ansehen" weitergeleitet. Sie können Ihr zuvor aufgenommenes Video ansehen.

Screen displaying story being played back to you.

Schlussfolgerung

Diese Anwendung ist bei weitem nicht perfekt. Sie werden feststellen, dass das aufgezeichnete Video kürzer ist als die Aufnahmesitzung. Das liegt daran, dass es Zeit braucht, um eine Anfrage zur Archivierung des Videos zu senden. Außerdem ist unsere Story-Liste eine einfache RecyclerView. Sie können sie in einen horizontalen Bildlauf umwandeln. Jede Story ist kreisförmig, so wie es bei Instagram Stories aussieht! Wir haben auch keine Authentifizierung und Autorisierung. Es gibt keinen Besitzer für die Story. Das Video wird für einige Zeit auf dem OpenTok-Server gespeichert. Danach wird das Video gelöscht. Sie können das Video an einem anderen Ort speichern, bevor das passiert.

Ressourcen

  • Sehen Sie sich unsere Dokumentation für die Vonage Video API hier

  • Der Code für diesen Blogbeitrag ist auf GitHub

Teilen Sie:

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