https://a.storyblok.com/f/270183/72742/79918a59b1/stories_videoapi_1200x600.png

Crie histórias no estilo das redes sociais com Android e Python

Publicado em March 23, 2021

Tempo de leitura: 19 minutos

O Instagram possui um recurso popular chamado “Stories”, fortemente inspirado no Snapchat. O “Stories” permite que os usuários criem um Video ou uma foto que desaparecerá em um curto período (24 horas). Posteriormente, outras plataformas de mídia social também desenvolveram esse recurso, como o LinkedIn e o Twitter.

Neste tutorial, vamos desenvolver esse recurso para Android usando a Video API da Vonage; também precisaremos de um servidor para gerenciar as sessões e os tokens do cliente, que será desenvolvido em Python.

Pré-requisitos

Montagem do servidor

Para começar, vamos criar um servidor com o qual nosso aplicativo móvel possa se comunicar. Esse servidor utilizará o Django, um framework web em Python.

Instalando dependências

Primeiro, crie um ambiente virtual de Python executando os dois comandos a seguir:

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

Em seguida, podemos instalar as dependências para o nosso servidor de back-end:

(.venv) $ pip install Django opentok

O comando acima instalará o Django e o Opentok, o SDK do Vonage Video para Python.

Criação de um projeto e de uma Application

Inicialize seu projeto Django executando o seguinte comando:

(.venv) $ django-admin startproject storiesserver

Antes de começarmos a escrever o código, precisamos criar pelo menos uma aplicação dentro do projeto Django. Um projeto Django pode consistir em várias aplicações. Por exemplo, o projeto A pode ser uma aplicação de Stories, enquanto o projeto B poderia ser uma aplicação de mensagens ou de comércio eletrônico. Primeiro, mude o diretório atual para o diretório do projeto e, em seguida, inicialize uma nova aplicação com os dois comandos mostrados abaixo:

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

Você não precisa executar o próximo comando, pois não vamos usar um banco de dados neste tutorial. No entanto, um aviso continuará aparecendo até que executemos o comando de migração; portanto, execute o seguinte para eliminar esses avisos.

(.venv) $ python manage.py migrate

Para garantir que nossa aplicação web em Django funcione, você pode executar o script do servidor web.

(.venv) $ python manage.py runserver

Você pode acessar seu aplicativo web no localhost, na porta 8000. No seu navegador, a página aparecerá assim http://localhost:8000. Se você acessar essa URL, verá uma mensagem de sucesso e a imagem de um foguete.

Variáveis de ambiente

Nosso código precisará usar sua chave e seu segredo da API do Vonage Video (também conhecido como OpenTok), que nunca devem ser codificados diretamente no seu aplicativo. Para aumentar a segurança do aplicativo, você pode usar variáveis de ambiente. No Terminal, você pode definir esses dois valores conforme mostrado nos exemplos abaixo. Certifique-se de substituir xxxxxx pelos seus valores. Você pode obter esses valores para a chave e o segredo da API na documentação da Video API do Vonage.

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

Criar as visualizações

As visualizações são os modelos que são renderizados na sua aplicação Django. Abra o storiesserver/storiesapp/views.py arquivo e substitua seu conteúdo pelo seguinte:

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

A primeira parte deste código cria uma instância do OpenTok, que utilizará nossas variáveis de ambiente pré-definidas para a chave da API e o segredo da API.

Então, temos duas variáveis globais, videos e index. Elas são como um minibanco de dados na memória que armazena as informações das nossas histórias (ou Videos).

No primeiro método, geramos nossa sessão e nosso token com o get_token método. Para criar uma sessão, usamos o create_session método da instância do OpenTok. Ele aceita dois parâmetros: um modo de mídia e um modo de arquivamento. Usamos o MediaModes.routed valor para o modo de mídia, pois publicamos um Video como uma “Story”, e não para um bate-papo por vídeo. MediaModes.routed significa que enviamos o Video para o servidor, em vez de para outro cliente. Usamos o ArchiveModes.manual valor para o modo de arquivamento, pois queremos arquivar (gravar) o Video manualmente para obter o ID do arquivo. Esse ID é importante quando quisermos recuperar o Video gravado. Caso contrário, teríamos que procurá-lo na lista de arquivos, o que é inconveniente. Para gerar o token, usamos o generate_token método da instância da sessão. Passamos o valor do tempo de validade como parâmetro dentro desse método. Por fim, enviamos a sessão, o token e a chave da API ao cliente.

No segundo método, o video_stream método, obtemos a URL do Video a partir do ID do arquivo. Se você notar, esse método possui um parâmetro adicional além do parâmetro usual, request. A obtenção do ID do arquivo é feita em outro método.

No terceiro método, o videos_list método, enviamos a lista dos nossos Videos para o cliente. Trata-se basicamente da nossa videos variável formatada em JSON.

No quarto método, o video_start_archive método, arquivamos o Video. Esse método possui um parâmetro adicional, session_id. Usamos o start_archive método da instância do OpenTok. Ele aceita uma sessão do OpenTok. Após executar esse método, nossa sessão de vídeo será gravada. O start_archive método retorna o ID do arquivo. Nós o armazenamos na videos variável. Além do ID do arquivo, geramos um nome adequado para esse Video, que pode ser algo como “História 1”, “História 2” e assim por diante.

No quinto método, o video_stop_archive método, interrompemos a gravação do Video. Esse método possui um parâmetro adicional, archive_id. Em outras palavras, interrompemos a gravação do Video. Usamos o stop_archive método da instância do OpenTok para realizar a tarefa. Ele aceita o ID do arquivo como parâmetro.

O último método, o homepage método, serve apenas para fins de teste, a fim de verificar se a aplicação web do Django pode ou não ser acessada.

Criar mapeamento de URLs

Em seguida, precisamos criar um mapeamento das URLs para esses métodos das visualizações. Crie o storiesserver/storiesapp/urls.py arquivo e copie o exemplo abaixo nele:

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

O path método aceita três argumentos:

Caminho da URL O método a ser acessado Um nome para a rota, para fins de referência.

Por exemplo, o segundo caminho mapeia o token URL ao get_token método nas visualizações.

No lado do servidor da aplicação Django, precisamos modificar o urls.py arquivo para mapear as URLs das histórias; portanto, localize o arquivo: storiesserver/storiesserver/urls.py e substitua o conteúdo desse arquivo por:

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

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

Neste arquivo, mapeamos a URL do aplicativo para a stories/ URL. Por exemplo, com a token URL, teríamos a seguinte URL completa: http://localhost:8000/stories/token.

Ngrok

Para disponibilizar a aplicação Django na Internet e em nossos dispositivos Android durante o desenvolvimento, usaremos o Ngrok. Após conectar sua Account, podemos iniciar um túnel HTTP que redireciona para a porta 8000 da nossa aplicação web Django:

$ ./ngrok http 8000

Quando o túnel for criado, você verá a URL pública, que terá um formato semelhante a este: https://xxxxxxxx.ngrok.io. Em seguida, você precisa modificar o storiesserver/storiesserver/settings.py arquivo, adicionando o "xxxxxxxx.ngrok.io", "10.0.2.2" e "localhost" cadeias de caracteres à ALLOWED_HOSTS variável. O 10.0.2.2 endereço é a forma como o emulador do Android acessa o localhost no seu computador.

Sua ALLOWED_HOSTS variável deve ficar assim após a alteração:

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

Então, para acessar a token URL, você acessaria esta URL: https://xxxxxxxx.ngrok.io/stories/token.

Nesta fase, você já desenvolveu o componente de servidor capaz de gerar tokens, arquivar gravações de vídeo e fornecer URLs dessas gravações. A seguir, você vai desenvolver um aplicativo para Android que funcionará como cliente desse aplicativo de servidor. Com esse aplicativo móvel, você poderá gravar vídeos e assistir às gravações.

Lado do cliente

Abra o Android Studio, crie um novo projeto e selecione “Empty Activity” como modelo do seu projeto. Para o SDK mínimo, escolha “API 16: Android 4.1 (Jelly Bean)” e, para a linguagem, escolha “Kotlin”.

Dependências

A primeira coisa a se fazer ao criar um projeto no Android Studio é adicionar todas as dependências necessárias. Abra o build.gradle no nível do projeto e adicione a seguinte linha dentro do repositories bloco localizado dentro do allprojects . Em seguida, sincronize o arquivo.

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

A segunda coisa que você precisa fazer é adicionar as dependências no arquivo, que tem o mesmo nome build.gradle , mas no nível do aplicativo (ou no nível do módulo). Adicione estas linhas dentro do dependencies bloco. Não se esqueça de sincronizar o arquivo.

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'

Este projeto utiliza cinco bibliotecas adicionais, a saber:

SDK de vídeo da Vonage (OpenTok); biblioteca OkHttp para enviar solicitações ao nosso servidor de back-end; biblioteca Gson para analisar respostas em JSON; biblioteca EasyPermissions para gerenciar permissões quando nosso aplicativo Android solicita permissão para usar a câmera e o microfone; biblioteca Coroutines para lidar com código não bloqueante e bloqueante de maneira conveniente.

Configuração de segurança de rede

Esta etapa é opcional. Se você quiser testar o aplicativo com um emulador e não quiser usar o Ngrok, será necessário criar um arquivo de configuração de rede. Crie o xml dentro do res diretório e, em seguida, crie um novo arquivo chamado network_security_config.xml arquivo dentro do xml diretório. Copie o seguinte nele:

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

Isso significa que você pode desenvolver o aplicativo sem o protocolo HTTPS para a URL do backend.

Manifesto do Android

Para ativar essa configuração de rede, abra o AndroidManifest.xml arquivo e adicione o seguinte atributo ao application nó:

android:networkSecurityConfig="@xml/network_security_config"

Em seguida, adicione o seguinte dentro do manifest nó.

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

Precisamos dessas duas adições porque queremos conectar nosso aplicativo móvel ao nosso servidor de back-end.

Já que estamos neste arquivo, vamos criar referências a mais duas atividades que serão criadas posteriormente. Adicione estas linhas após o único activity nó.

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

Layout

Temos três atividades, mas precisamos criar três layouts para elas. Também precisamos criar um layout adicional para o layout de linha em um RecyclerView usado na MainActivity.

Primeiro, crie o layout da linha criando um novo arquivo chamado row.xml dentro do app/src/main/res/layout diretório. Copie o seguinte XML para esse novo arquivo::

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

O código acima define a criação de um botão, que será usado para iniciar uma Activity para assistir à história ou ao Video.

Em seguida, precisamos criar um layout para exibir um Video. Crie um novo arquivo chamado activity_viewing_story.xml no diretório de layouts e adicione o seguinte código XML ao arquivo:

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

O layout contém um WebView que exibirá o Video.

Em seguida, precisamos criar o activity_creating_story.xml arquivo. Esse é o layout usado pela Activity para criar um Video. Apague o conteúdo do arquivo e adicione o seguinte código a ele:

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

O FrameLayout com o publisher ID é usado pelo SDK de Video para exibir o vídeo que está sendo enviado ao servidor de Video. O objetivo do botão nesta atividade é interromper a gravação do vídeo e encerrar a atividade.

Por fim, precisamos editar o activity_main.xml arquivo. Esse arquivo contém o layout usado pela MainActivity, que listará todas as histórias (Vídeos) que foram criadas. O arquivo também conterá um botão para iniciar a atividade de criação de um Vídeo. Substitua o conteúdo desse arquivo pelo seguinte código 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>

Adicionando a URL do servidor

Vamos definir a URL do servidor no strings.xml arquivo que pode ser encontrado no values diretório. Para isso, adicione o seguinte ao seu resources node:

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

No entanto, se você estiver usando o Ngrok, altere para o exemplo a seguir (certifique-se de substituir xxxxx pela sua URL do Ngrok):

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

Aulas e atividades

Primeiro, precisaremos criar as classes necessárias para um RecyclerView. Um RecyclerView precisa de um adaptador e de um contêiner. Crie um arquivo para o contêiner com o nome StoryViewHolder.kt dentro do package diretório. O pacote será algo semelhante a com.example.storiesapplication , localizado dentro do java diretório. Copie o código a seguir para esse novo arquivo:

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
	}

}

Para cada arquivo de classe ou Activity que você criar, certifique-se de alterar o com.example.storiesapplication pacote para o seu, caso ele seja diferente.

Esta é uma classe de contêiner padrão na qual você define o texto do botão por meio de um parâmetro de string e configura uma função de retorno de chamada para o botão.

Depois de criar a classe “holder”, precisamos de uma classe adaptadora. Um adaptador é uma ponte entre os dados e a visualização do RecyclerView. Crie um novo arquivo dentro do java/package/ diretório chamado StoryAdapter.kt file e adicione o seguinte código a ele:

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
}

O onCreateViewHolder método infla o row layout e cria uma instância da classe de contenção. O onBindViewHolder método define os dados em uma linha específica do RecyclerView, e o getItemCount método retorna o número de itens armazenados.

Agora é hora de criar o RecyclerView na MainActivity. Modifique o MainActivity.kt arquivo, substituindo o conteúdo pelo seguinte:

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
)

O onCreate método adiciona um callback ao botão flutuante nesta Activity, que chamará o getToken método para obter o token, a sessão e a chave da API antes de passá-los para a CreatingStoryActivity Activity. O RecyclerView também carrega a lista de vídeos e, para cada linha, adicionamos um callback para chamar a ViewingStoryActivity Activity.

Em seguida, precisamos criar o CreatingStoryActivity.kt arquivo no app/src/main/java/com/example/storiesapplication diretório (o mesmo diretório onde o MainActivity.kt arquivo está localizado), que é a Activity que publica um Video no servidor OpenTok. Copie o código a seguir para esse novo arquivo::

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

No onCreate método acima, solicitamos permissões para a câmera, internet e gravação de áudio. Se tivermos as permissões, criamos um objeto de sessão usando o token, a sessão e a chave da API. Também precisamos definir o ouvinte para esse objeto de sessão. Além disso, definimos um callback para o botão nesta Activity. O botão enviará uma solicitação para interromper o arquivamento do Video.

Temos vários métodos exigidos pelo ouvinte de sessão. O mais importante deles é o onConnected método, que será chamado caso nossa sessão tenha se conectado ao servidor OpenTok.

Criamos um objeto “publisher” e definimos o FrameLayout como a visualização desse “publisher”. Também precisamos conectar nosso objeto “session” a esse objeto “publisher”. Em seguida, criamos um objeto “request” para iniciar o arquivamento do Video.

Em seguida, precisamos criar o ViewingStoryActivity.kt arquivo no app/src/main/java/com/example/storiesapplication diretório (o mesmo diretório onde o MainActivity.kt arquivo está localizado), que é a Activity responsável pela exibição do Video; portanto, adicione o seguinte código a esse arquivo::

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

Primeiramente, obtemos a URL do Video a partir do ID do arquivo, enviando uma solicitação à nossa aplicação Django. Em seguida, carregamos a URL no WebView.

Iniciando o aplicativo

Inicie o aplicativo. Lembre-se de usar o Ngrok ou de implantar o aplicativo Django na nuvem caso queira usar um dispositivo Android. No início, você verá uma tela vazia e um botão flutuante.

Main Android screen with floating button

Aperte o botão flutuante e o dispositivo transmitirá um Video da sua câmera para o servidor OpenTok. Quando tiver gravado o suficiente para a sua história, aperte o botão “Enviar história” na tela.

Android screen displaying what's being captured with your camera

Você será redirecionado de volta para a tela principal, que agora exibe uma lista das suas histórias.

Main Android screen now displaying your stories

Se você tocar na linha da história, será redirecionado para a tela de visualização da história. Você poderá assistir ao Video que gravou anteriormente.

Screen displaying story being played back to you.

Conclusão

Este aplicativo está longe de ser perfeito. Se você notar, o vídeo gravado é mais curto do que a sessão de gravação. Isso acontece porque leva algum tempo para enviar uma solicitação para arquivar o vídeo. Além disso, nossa lista de stories é um simples RecyclerView. Você pode convertê-la para uma rolagem horizontal. Cada história é apresentada em um formato circular, exatamente como nas Histórias do Instagram! Também não temos autenticação nem autorização. Não há um proprietário para a história. Esse Video fica armazenado no servidor do OpenTok por algum tempo. Depois disso, o Video será excluído. Você pode optar por salvar o Video em outro lugar antes que isso aconteça.

Recursos

  • Confira nossa documentação sobre a Video API da Vonage aqui

  • O código desta postagem do blog está no GitHub

Compartilhar:

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