
Compartilhar:
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
Crie histórias no estilo das redes sociais com Android e Python
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
Dispositivo Android (opcional)
Ngrok (opcional)
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:
Em seguida, podemos instalar as dependências para o nosso servidor de back-end:
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:
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:
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.
Para garantir que nossa aplicação web em Django funcione, você pode executar o script do servidor web.
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.
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:
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.

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.

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

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.

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.