Timeline : extract video thumbnail and upload it

This commit is contained in:
ganfra 2019-04-16 17:24:28 +02:00
parent 2c83ba0824
commit dab80466c5
8 changed files with 212 additions and 73 deletions

View File

@ -27,8 +27,8 @@ data class ContentAttachmentData(
val height: Long? = 0, val height: Long? = 0,
val width: Long? = 0, val width: Long? = 0,
val name: String? = null, val name: String? = null,
val path: String? = null, val path: String,
val mimeType: String? = null, val mimeType: String,
val type: Type val type: Type
) : Parcelable { ) : Parcelable {



View File

@ -18,9 +18,15 @@ package im.vector.matrix.android.api.session.content


interface ContentUploadStateTracker { interface ContentUploadStateTracker {


fun track(eventId: String, updateListener: UpdateListener) fun track(key: String, updateListener: UpdateListener)


fun untrack(eventId: String, updateListener: UpdateListener) fun untrack(key: String, updateListener: UpdateListener)

fun setFailure(key: String)

fun setSuccess(key: String)

fun setProgress(key: String, current: Long, total: Long)


interface UpdateListener { interface UpdateListener {
fun onUpdate(state: State) fun onUpdate(state: State)

View File

@ -31,7 +31,7 @@ internal class ContentModule {
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
ContentUploader(get(), get(), get<ContentUploadStateTracker>() as DefaultContentUploadStateTracker) FileUploader(get(), get())
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {

View File

@ -23,42 +23,42 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
internal class DefaultContentUploadStateTracker : ContentUploadStateTracker { internal class DefaultContentUploadStateTracker : ContentUploadStateTracker {


private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
private val progressByEvent = mutableMapOf<String, ContentUploadStateTracker.State>() private val states = mutableMapOf<String, ContentUploadStateTracker.State>()
private val listenersByEvent = mutableMapOf<String, MutableList<ContentUploadStateTracker.UpdateListener>>() private val listeners = mutableMapOf<String, MutableList<ContentUploadStateTracker.UpdateListener>>()


override fun track(eventId: String, updateListener: ContentUploadStateTracker.UpdateListener) { override fun track(key: String, updateListener: ContentUploadStateTracker.UpdateListener) {
val listeners = listenersByEvent[eventId] ?: ArrayList() val listeners = listeners[key] ?: ArrayList()
listeners.add(updateListener) listeners.add(updateListener)
listenersByEvent[eventId] = listeners this.listeners[key] = listeners
val currentState = progressByEvent[eventId] ?: ContentUploadStateTracker.State.Idle val currentState = states[key] ?: ContentUploadStateTracker.State.Idle
mainHandler.post { updateListener.onUpdate(currentState) } mainHandler.post { updateListener.onUpdate(currentState) }
} }


override fun untrack(eventId: String, updateListener: ContentUploadStateTracker.UpdateListener) { override fun untrack(key: String, updateListener: ContentUploadStateTracker.UpdateListener) {
listenersByEvent[eventId]?.apply { listeners[key]?.apply {
remove(updateListener) remove(updateListener)
} }
} }


internal fun setFailure(eventId: String) { override fun setFailure(key: String) {
val failure = ContentUploadStateTracker.State.Failure val failure = ContentUploadStateTracker.State.Failure
updateState(eventId, failure) updateState(key, failure)
} }


internal fun setSuccess(eventId: String) { override fun setSuccess(key: String) {
val success = ContentUploadStateTracker.State.Success val success = ContentUploadStateTracker.State.Success
updateState(eventId, success) updateState(key, success)
} }


internal fun setProgress(eventId: String, current: Long, total: Long) { override fun setProgress(key: String, current: Long, total: Long) {
val progressData = ContentUploadStateTracker.State.ProgressData(current, total) val progressData = ContentUploadStateTracker.State.ProgressData(current, total)
updateState(eventId, progressData) updateState(key, progressData)
} }


private fun updateState(eventId: String, state: ContentUploadStateTracker.State) { private fun updateState(key: String, state: ContentUploadStateTracker.State) {
progressByEvent[eventId] = state states[key] = state
mainHandler.post { mainHandler.post {
listenersByEvent[eventId]?.also { listeners -> listeners[key]?.also { listeners ->
listeners.forEach { it.onUpdate(state) } listeners.forEach { it.onUpdate(state) }
} }
} }

View File

@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.content
import arrow.core.Try import arrow.core.Try
import arrow.core.Try.Companion.raise import arrow.core.Try.Companion.raise
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.network.ProgressRequestBody import im.vector.matrix.android.internal.network.ProgressRequestBody
import okhttp3.HttpUrl import okhttp3.HttpUrl
@ -31,44 +30,51 @@ import java.io.File
import java.io.IOException import java.io.IOException




internal class ContentUploader(private val okHttpClient: OkHttpClient, internal class FileUploader(private val okHttpClient: OkHttpClient,
private val sessionParams: SessionParams, private val sessionParams: SessionParams) {
private val contentUploadProgressTracker: DefaultContentUploadStateTracker) {
private val uploadUrl = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload"


private val moshi = MoshiProvider.providesMoshi() private val moshi = MoshiProvider.providesMoshi()
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java) private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)


fun uploadFile(eventId: String, attachment: ContentAttachmentData): Try<ContentUploadResponse> {
if (attachment.path == null || attachment.mimeType == null) {
return raise(RuntimeException())
}
val file = File(attachment.path)
val urlString = sessionParams.homeServerConnectionConfig.homeServerUri.toString() + URI_PREFIX_CONTENT_API + "upload"


val urlBuilder = HttpUrl.parse(urlString)?.newBuilder() fun uploadFile(file: File,
?: return raise(RuntimeException()) filename: String?,
mimeType: String,
progressListener: ProgressRequestBody.Listener? = null): Try<ContentUploadResponse> {

val uploadBody = RequestBody.create(MediaType.parse(mimeType), file)
return upload(uploadBody, filename, progressListener)

}

fun uploadByteArray(byteArray: ByteArray,
filename: String?,
mimeType: String,
progressListener: ProgressRequestBody.Listener? = null): Try<ContentUploadResponse> {

val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray)
return upload(uploadBody, filename, progressListener)

}


private fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): Try<ContentUploadResponse> {
val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: return raise(RuntimeException())


val httpUrl = urlBuilder val httpUrl = urlBuilder
.addQueryParameter( .addQueryParameter("filename", filename)
"filename", attachment.name .build()
).build()


val requestBody = RequestBody.create( val requestBody = if (progressListener != null) ProgressRequestBody(uploadBody, progressListener) else uploadBody
MediaType.parse(attachment.mimeType),
file
)
val progressRequestBody = ProgressRequestBody(requestBody, object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) {
contentUploadProgressTracker.setProgress(eventId, current, total)
}
})


val request = Request.Builder() val request = Request.Builder()
.url(httpUrl) .url(httpUrl)
.post(progressRequestBody) .post(requestBody)
.build() .build()


val result = Try { return Try {
okHttpClient.newCall(request).execute().use { response -> okHttpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) { if (!response.isSuccessful) {
throw IOException() throw IOException()
@ -80,11 +86,7 @@ internal class ContentUploader(private val okHttpClient: OkHttpClient,
} }
} }
} }
if (result.isFailure()) {
contentUploadProgressTracker.setFailure(eventId)
} else {
contentUploadProgressTracker.setSuccess(eventId)
}
return result
} }

} }

View File

@ -0,0 +1,68 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.session.content

import android.graphics.Bitmap
import android.media.ThumbnailUtils
import android.provider.MediaStore
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import java.io.ByteArrayOutputStream
import java.io.File

internal object ThumbnailExtractor {

class ThumbnailData(
val width: Int,
val height: Int,
val size: Long,
val bytes: ByteArray,
val mimeType: String
)

fun extractThumbnail(attachment: ContentAttachmentData): ThumbnailData? {
val file = File(attachment.path)
if (!file.exists() || !file.isFile) {
return null
}
return if (attachment.type == ContentAttachmentData.Type.VIDEO) {
extractVideoThumbnail(attachment)
} else {
null
}
}

private fun extractVideoThumbnail(attachment: ContentAttachmentData): ThumbnailData? {
val thumbnail = ThumbnailUtils.createVideoThumbnail(attachment.path, MediaStore.Video.Thumbnails.MINI_KIND)
val outputStream = ByteArrayOutputStream()
thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
val thumbnailWidth = thumbnail.width
val thumbnailHeight = thumbnail.height
val thumbnailSize = outputStream.size()
val thumbnailData = ThumbnailData(
width = thumbnailWidth,
height = thumbnailHeight,
size = thumbnailSize.toLong(),
bytes = outputStream.toByteArray(),
mimeType = "image/jpeg"
)
thumbnail.recycle()
outputStream.reset()
return thumbnailData
}


}

View File

@ -21,6 +21,7 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
@ -30,14 +31,19 @@ import im.vector.matrix.android.api.session.room.model.message.MessageFileConten
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.network.ProgressRequestBody
import im.vector.matrix.android.internal.session.room.send.SendEventWorker import im.vector.matrix.android.internal.session.room.send.SendEventWorker
import im.vector.matrix.android.internal.util.WorkerParamsFactory import im.vector.matrix.android.internal.util.WorkerParamsFactory
import org.koin.standalone.inject import org.koin.standalone.inject
import timber.log.Timber
import java.io.File



internal class UploadContentWorker(context: Context, params: WorkerParameters) internal class UploadContentWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params), MatrixKoinComponent { : CoroutineWorker(context, params), MatrixKoinComponent {


private val mediaUploader by inject<ContentUploader>() private val fileUploader by inject<FileUploader>()
private val contentUploadProgressTracker by inject<ContentUploadStateTracker>()


@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
@ -50,29 +56,63 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters)
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.failure() ?: return Result.failure()


if (params.event.eventId == null) { val eventId = params.event.eventId ?: return Result.failure()
return Result.failure() val attachment = params.attachment

val thumbnailData = ThumbnailExtractor.extractThumbnail(params.attachment)
val attachmentFile = createAttachmentFile(attachment) ?: return Result.failure()
var uploadedThumbnailUrl: String? = null

if (thumbnailData != null) {
fileUploader
.uploadByteArray(thumbnailData.bytes, "thumb_${attachment.name}", thumbnailData.mimeType)
.fold(
{ Timber.e(it) },
{ uploadedThumbnailUrl = it.contentUri }
)
} }
return mediaUploader
.uploadFile(params.event.eventId, params.attachment) val progressListener = object : ProgressRequestBody.Listener {
.fold({ handleFailure() }, { handleSuccess(params, it) }) override fun onProgress(current: Long, total: Long) {
contentUploadProgressTracker.setProgress(eventId, current, total)
}
}
return fileUploader
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
.fold(
{ handleFailure(params) },
{ handleSuccess(params, it.contentUri, uploadedThumbnailUrl) }
)
} }


private fun handleFailure(): Result { private fun createAttachmentFile(attachment: ContentAttachmentData): File? {
return Result.retry() return try {
File(attachment.path)
} catch (e: Exception) {
Timber.e(e)
null
}
} }


private fun handleSuccess(params: Params, contentUploadResponse: ContentUploadResponse): Result { private fun handleFailure(params: Params): Result {
val event = updateEvent(params.event, contentUploadResponse.contentUri) contentUploadProgressTracker.setFailure(params.event.eventId!!)
return Result.failure()
}

private fun handleSuccess(params: Params,
attachmentUrl: String,
thumbnailUrl: String?): Result {
contentUploadProgressTracker.setFailure(params.event.eventId!!)
val event = updateEvent(params.event, attachmentUrl, thumbnailUrl)
val sendParams = SendEventWorker.Params(params.roomId, event) val sendParams = SendEventWorker.Params(params.roomId, event)
return Result.success(WorkerParamsFactory.toData(sendParams)) return Result.success(WorkerParamsFactory.toData(sendParams))
} }


private fun updateEvent(event: Event, url: String): Event { private fun updateEvent(event: Event, url: String, thumbnailUrl: String? = null): Event {
val messageContent: MessageContent = event.content.toModel() ?: return event val messageContent: MessageContent = event.content.toModel() ?: return event
val updatedContent = when (messageContent) { val updatedContent = when (messageContent) {
is MessageImageContent -> messageContent.update(url) is MessageImageContent -> messageContent.update(url)
is MessageVideoContent -> messageContent.update(url) is MessageVideoContent -> messageContent.update(url, thumbnailUrl)
is MessageFileContent -> messageContent.update(url) is MessageFileContent -> messageContent.update(url)
is MessageAudioContent -> messageContent.update(url) is MessageAudioContent -> messageContent.update(url)
else -> messageContent else -> messageContent
@ -84,8 +124,8 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters)
return copy(url = url) return copy(url = url)
} }


private fun MessageVideoContent.update(url: String): MessageVideoContent { private fun MessageVideoContent.update(url: String, thumbnailUrl: String?): MessageVideoContent {
return copy(url = url) return copy(url = url, info = info?.copy(thumbnailUrl = thumbnailUrl))
} }


private fun MessageFileContent.update(url: String): MessageFileContent { private fun MessageFileContent.update(url: String): MessageFileContent {

View File

@ -16,6 +16,7 @@


package im.vector.matrix.android.internal.session.room.send package im.vector.matrix.android.internal.session.room.send


import android.media.MediaMetadataRetriever
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.content.ContentAttachmentData
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
@ -30,7 +31,9 @@ import im.vector.matrix.android.api.session.room.model.message.MessageImageConte
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo
import im.vector.matrix.android.api.session.room.model.message.VideoInfo import im.vector.matrix.android.api.session.room.model.message.VideoInfo
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor


internal class LocalEchoEventFactory(private val credentials: Credentials) { internal class LocalEchoEventFactory(private val credentials: Credentials) {


@ -53,7 +56,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) {
type = MessageType.MSGTYPE_IMAGE, type = MessageType.MSGTYPE_IMAGE,
body = attachment.name ?: "image", body = attachment.name ?: "image",
info = ImageInfo( info = ImageInfo(
mimeType = attachment.mimeType ?: "image/png", mimeType = attachment.mimeType,
width = attachment.width?.toInt() ?: 0, width = attachment.width?.toInt() ?: 0,
height = attachment.height?.toInt() ?: 0, height = attachment.height?.toInt() ?: 0,
size = attachment.size.toInt() size = attachment.size.toInt()
@ -64,15 +67,35 @@ internal class LocalEchoEventFactory(private val credentials: Credentials) {
} }


private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event { private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData): Event {
val mediaDataRetriever = MediaMetadataRetriever()
mediaDataRetriever.setDataSource(attachment.path)

// Use frame to calculate height and width as we are sure to get the right ones
val firstFrame = mediaDataRetriever.frameAtTime
val height = firstFrame.height
val width = firstFrame.width
mediaDataRetriever.release()

val thumbnailInfo = ThumbnailExtractor.extractThumbnail(attachment)?.let {
ThumbnailInfo(
width = it.width,
height = it.height,
size = it.size,
mimeType = it.mimeType
)
}
val content = MessageVideoContent( val content = MessageVideoContent(
type = MessageType.MSGTYPE_VIDEO, type = MessageType.MSGTYPE_VIDEO,
body = attachment.name ?: "video", body = attachment.name ?: "video",
info = VideoInfo( info = VideoInfo(
mimeType = attachment.mimeType ?: "video/mpeg", mimeType = attachment.mimeType,
width = attachment.width?.toInt() ?: 0, width = width,
height = attachment.height?.toInt() ?: 0, height = height,
size = attachment.size, size = attachment.size,
duration = attachment.duration?.toInt() ?: 0 duration = attachment.duration?.toInt() ?: 0,
// Glide will be able to use the local path and extract a thumbnail.
thumbnailUrl = attachment.path,
thumbnailInfo = thumbnailInfo
), ),
url = attachment.path url = attachment.path
) )