added video encoding

This commit is contained in:
Mariotaku Lee 2017-01-23 13:27:29 +08:00
parent c34ca2fac6
commit 36fb5f223a
No known key found for this signature in database
GPG Key ID: 9C0706AE47FCE2AD
5 changed files with 144 additions and 58 deletions

View File

@ -158,7 +158,7 @@ dependencies {
compile 'com.bluelinelabs:logansquare:1.3.7' compile 'com.bluelinelabs:logansquare:1.3.7'
compile 'com.soundcloud.android:android-crop:1.0.1@aar' compile 'com.soundcloud.android:android-crop:1.0.1@aar'
compile 'com.hannesdorfmann.parcelableplease:annotation:1.0.2' compile 'com.hannesdorfmann.parcelableplease:annotation:1.0.2'
compile 'com.github.mariotaku:PickNCrop:0.9.12' compile 'com.github.mariotaku:PickNCrop:0.9.13'
compile "com.github.mariotaku.RestFu:library:$mariotaku_restfu_version" compile "com.github.mariotaku.RestFu:library:$mariotaku_restfu_version"
compile "com.github.mariotaku.RestFu:okhttp3:$mariotaku_restfu_version" compile "com.github.mariotaku.RestFu:okhttp3:$mariotaku_restfu_version"
compile 'com.squareup.okhttp3:okhttp:3.5.0' compile 'com.squareup.okhttp3:okhttp:3.5.0'
@ -166,6 +166,7 @@ dependencies {
compile 'com.google.dagger:dagger:2.8' compile 'com.google.dagger:dagger:2.8'
compile 'org.attoparser:attoparser:2.0.1.RELEASE' compile 'org.attoparser:attoparser:2.0.1.RELEASE'
compile 'com.getkeepsafe.taptargetview:taptargetview:1.6.0' compile 'com.getkeepsafe.taptargetview:taptargetview:1.6.0'
compile 'net.ypresto.androidtranscoder:android-transcoder:0.2.0'
compile 'com.github.mariotaku.MediaViewerLibrary:base:0.9.17' compile 'com.github.mariotaku.MediaViewerLibrary:base:0.9.17'
compile 'com.github.mariotaku.MediaViewerLibrary:subsample-image-view:0.9.17' compile 'com.github.mariotaku.MediaViewerLibrary:subsample-image-view:0.9.17'
compile 'com.github.mariotaku.SQLiteQB:library:0.9.8' compile 'com.github.mariotaku.SQLiteQB:library:0.9.8'

View File

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="org.mariotaku.twidere" package="org.mariotaku.twidere"
android:installLocation="internalOnly"> android:installLocation="internalOnly">
<uses-sdk tools:overrideLibrary="android.support.customtabs" /> <uses-sdk tools:overrideLibrary="android.support.customtabs,net.ypresto.androidtranscoder" />
<uses-feature <uses-feature
android:name="android.hardware.camera" android:name="android.hardware.camera"

View File

@ -1121,6 +1121,7 @@ class ComposeActivity : BaseActivity(), OnMenuItemClickListener, OnClickListener
.containsVideo(true) .containsVideo(true)
.videoOnly(false) .videoOnly(false)
.allowMultiple(true) .allowMultiple(true)
.videoQuality(0) // Low quality
.build() .build()
startActivityForResult(intent, REQUEST_PICK_MEDIA) startActivityForResult(intent, REQUEST_PICK_MEDIA)
return true return true

View File

@ -37,7 +37,6 @@ import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationCompat.Builder import android.support.v4.app.NotificationCompat.Builder
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import android.util.Pair
import android.widget.Toast import android.widget.Toast
import edu.tsinghua.hotmobi.HotMobiLogger import edu.tsinghua.hotmobi.HotMobiLogger
import edu.tsinghua.hotmobi.model.TimelineType import edu.tsinghua.hotmobi.model.TimelineType
@ -279,7 +278,7 @@ class LengthyOperationsService : BaseIntentService("lengthy_operations") {
} }
}) })
task.callback = this task.callback = this
task.params = Pair.create(actionType, item) task.params = Pair(actionType, item)
handler.post { ManualTaskStarter.invokeBeforeExecute(task) } handler.post { ManualTaskStarter.invokeBeforeExecute(task) }
val result = ManualTaskStarter.invokeExecute(task) val result = ManualTaskStarter.invokeExecute(task)
@ -330,7 +329,7 @@ class LengthyOperationsService : BaseIntentService("lengthy_operations") {
else -> { else -> {
if (imageUri != null) { if (imageUri != null) {
val mediaUri = Uri.parse(imageUri) val mediaUri = Uri.parse(imageUri)
var bodyAndSize: Pair<Body, Point>? = null var bodyAndSize: Pair<Body, Point?>? = null
try { try {
bodyAndSize = UpdateStatusTask.getBodyFromMedia(this, mediaLoader, bodyAndSize = UpdateStatusTask.getBodyFromMedia(this, mediaLoader,
mediaUri, null, ParcelableMedia.Type.IMAGE, mediaUri, null, ParcelableMedia.Type.IMAGE,

View File

@ -1,6 +1,5 @@
package org.mariotaku.twidere.task.twitter package org.mariotaku.twidere.task.twitter
import android.content.ContentProvider
import android.content.ContentResolver import android.content.ContentResolver
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
@ -8,15 +7,17 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.Point import android.graphics.Point
import android.net.Uri import android.net.Uri
import android.os.Build
import android.support.annotation.UiThread import android.support.annotation.UiThread
import android.support.annotation.WorkerThread import android.support.annotation.WorkerThread
import android.text.TextUtils import android.text.TextUtils
import android.util.Pair
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import com.nostra13.universalimageloader.core.DisplayImageOptions import com.nostra13.universalimageloader.core.DisplayImageOptions
import com.nostra13.universalimageloader.core.assist.ImageSize import com.nostra13.universalimageloader.core.assist.ImageSize
import edu.tsinghua.hotmobi.HotMobiLogger import edu.tsinghua.hotmobi.HotMobiLogger
import edu.tsinghua.hotmobi.model.MediaUploadEvent import edu.tsinghua.hotmobi.model.MediaUploadEvent
import net.ypresto.androidtranscoder.MediaTranscoder
import net.ypresto.androidtranscoder.format.MediaFormatStrategyPresets
import org.apache.commons.lang3.ArrayUtils import org.apache.commons.lang3.ArrayUtils
import org.apache.commons.lang3.math.NumberUtils import org.apache.commons.lang3.math.NumberUtils
import org.mariotaku.abstask.library.AbstractTask import org.mariotaku.abstask.library.AbstractTask
@ -46,7 +47,10 @@ import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper import org.mariotaku.twidere.util.dagger.GeneralComponentHelper
import org.mariotaku.twidere.util.io.ContentLengthInputStream import org.mariotaku.twidere.util.io.ContentLengthInputStream
import org.mariotaku.twidere.util.io.DirectByteArrayOutputStream import org.mariotaku.twidere.util.io.DirectByteArrayOutputStream
import java.io.* import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -250,7 +254,7 @@ class UpdateStatusTask(
val account = statusUpdate.accounts[i] val account = statusUpdate.accounts[i]
result.accountTypes[i] = account.type result.accountTypes[i] = account.type
val microBlog = MicroBlogAPIFactory.getInstance(context, account.key) val microBlog = MicroBlogAPIFactory.getInstance(context, account.key)
var bodyAndSize: Pair<Body, Point>? = null var bodyAndSize: Pair<Body, Point?>? = null
try { try {
when (account.type) { when (account.type) {
AccountType.FANFOU -> { AccountType.FANFOU -> {
@ -261,7 +265,7 @@ class UpdateStatusTask(
result.exceptions[i] = MicroBlogException( result.exceptions[i] = MicroBlogException(
context.getString(R.string.error_too_many_photos_fanfou)) context.getString(R.string.error_too_many_photos_fanfou))
} else { } else {
val sizeLimit = Point(2048, 1536) val sizeLimit = SizeLimit(width = 2048, height = 1536)
bodyAndSize = getBodyFromMedia(context, mediaLoader, bodyAndSize = getBodyFromMedia(context, mediaLoader,
Uri.parse(statusUpdate.media[0].uri), Uri.parse(statusUpdate.media[0].uri),
sizeLimit, statusUpdate.media[0].type, sizeLimit, statusUpdate.media[0].type,
@ -443,16 +447,18 @@ class UpdateStatusTask(
val mediaIds = update.media.mapIndexed { index, media -> val mediaIds = update.media.mapIndexed { index, media ->
val resp: MediaUploadResponse val resp: MediaUploadResponse
//noinspection TryWithIdenticalCatches //noinspection TryWithIdenticalCatches
var bodyAndSize: Pair<Body, Point>? = null var bodyAndSize: Pair<Body, Point?>? = null
try { try {
val sizeLimit = Point(2048, 1536) val sizeLimit = SizeLimit(width = 2048, height = 1536)
bodyAndSize = getBodyFromMedia(context, mediaLoader, Uri.parse(media.uri), sizeLimit, bodyAndSize = getBodyFromMedia(context, mediaLoader, Uri.parse(media.uri), sizeLimit,
media.type, ContentLengthInputStream.ReadListener { length, position -> media.type, ContentLengthInputStream.ReadListener { length, position ->
stateCallback.onUploadingProgressChanged(index, position, length) stateCallback.onUploadingProgressChanged(index, position, length)
}) })
val mediaUploadEvent = MediaUploadEvent.create(context, media) val mediaUploadEvent = MediaUploadEvent.create(context, media)
mediaUploadEvent.setFileSize(bodyAndSize.first.length()) mediaUploadEvent.setFileSize(bodyAndSize.first.length())
mediaUploadEvent.setGeometry(bodyAndSize.second.x, bodyAndSize.second.y) bodyAndSize.second?.let { geometry ->
mediaUploadEvent.setGeometry(geometry.x, geometry.y)
}
if (chucked) { if (chucked) {
resp = uploadMediaChucked(upload, bodyAndSize.first, ownerIds) resp = uploadMediaChucked(upload, bodyAndSize.first, ownerIds)
} else { } else {
@ -686,17 +692,23 @@ class UpdateStatusTask(
fun beforeExecute() fun beforeExecute()
} }
data class SizeLimit(val width: Int, val height: Int)
companion object { companion object {
private val BULK_SIZE = 256 * 1024// 128 Kib private val BULK_SIZE = 256 * 1024// 128 Kib
@Throws(IOException::class) @Throws(IOException::class)
fun getBodyFromMedia(context: Context, mediaLoader: MediaLoaderWrapper, fun getBodyFromMedia(
mediaUri: Uri, sizeLimit: Point? = null, context: Context,
@ParcelableMedia.Type type: Int, mediaLoader: MediaLoaderWrapper,
readListener: ContentLengthInputStream.ReadListener): Pair<Body, Point> { mediaUri: Uri,
sizeLimit: SizeLimit? = null,
@ParcelableMedia.Type type: Int,
readListener: ContentLengthInputStream.ReadListener
): Pair<Body, Point?> {
val resolver = context.contentResolver val resolver = context.contentResolver
var mediaType = resolver.getType(mediaUri) ?: run { val mediaType = resolver.getType(mediaUri) ?: run {
if (mediaUri.scheme == ContentResolver.SCHEME_FILE) { if (mediaUri.scheme == ContentResolver.SCHEME_FILE) {
mediaUri.lastPathSegment?.substringAfterLast(".")?.let { ext -> mediaUri.lastPathSegment?.substringAfterLast(".")?.let { ext ->
return@run MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) return@run MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
@ -704,59 +716,131 @@ class UpdateStatusTask(
} }
return@run null return@run null
} }
val size = Point() val data = run {
val cis = run { if (sizeLimit != null) {
if (type == ParcelableMedia.Type.IMAGE && sizeLimit != null) { when (type) {
val length: Long ParcelableMedia.Type.IMAGE -> {
val o = BitmapFactory.Options() return@run imageStream(resolver, mediaLoader, mediaUri, mediaType, sizeLimit)
o.inJustDecodeBounds = true }
BitmapFactoryUtils.decodeUri(resolver, mediaUri, null, o) ParcelableMedia.Type.VIDEO -> {
if (o.outMimeType != null) { return@run videoStream(context, resolver, mediaUri, mediaType)
mediaType = o.outMimeType
}
size.set(o.outWidth, o.outHeight)
o.inSampleSize = Utils.calculateInSampleSize(o.outWidth, o.outHeight,
sizeLimit.x, sizeLimit.y)
o.inJustDecodeBounds = false
if (o.outWidth > 0 && o.outHeight > 0 && mediaType != "image/gif") {
val displayOptions = DisplayImageOptions.Builder()
.considerExifParams(true)
.build()
val bitmap = mediaLoader.loadImageSync(mediaUri.toString(),
ImageSize(o.outWidth, o.outHeight).scaleDown(o.inSampleSize),
displayOptions)
if (bitmap != null) {
size.set(bitmap.width, bitmap.height)
val os = DirectByteArrayOutputStream()
when (mediaType) {
"image/png", "image/x-png", "image/webp", "image-x-webp" -> {
bitmap.compress(Bitmap.CompressFormat.PNG, 0, os)
}
else -> {
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, os)
}
}
length = os.size().toLong()
return@run ContentLengthInputStream(os.inputStream(true), length)
} }
} }
} }
return@run null
}
val cis = data?.stream ?: run {
val st = resolver.openInputStream(mediaUri) ?: throw FileNotFoundException(mediaUri.toString()) val st = resolver.openInputStream(mediaUri) ?: throw FileNotFoundException(mediaUri.toString())
val length = st.available().toLong() val length = st.available().toLong()
return@run ContentLengthInputStream(st, length) return@run TypedContentLengthInputStream(st, length, null)
} }
cis.setReadListener(readListener) cis.setReadListener(readListener)
val contentType = if (mediaType != null) { val contentType = if (cis.type != null) {
ContentType.parse(cis.type)
} else if (mediaType != null) {
ContentType.parse(mediaType) ContentType.parse(mediaType)
} else { } else {
ContentType.parse("application/octet-stream") ContentType.parse("application/octet-stream")
} }
return Pair(FileBody(cis, "attachment", cis.length(), contentType), size) return Pair(FileBody(cis, "attachment", cis.length(), contentType), data?.geometry)
} }
private fun imageStream(
resolver: ContentResolver,
mediaLoader: MediaLoaderWrapper,
mediaUri: Uri,
defaultType: String?,
sizeLimit: SizeLimit
): MediaStreamData? {
val length: Long
var mediaType = defaultType
val o = BitmapFactory.Options()
o.inJustDecodeBounds = true
BitmapFactoryUtils.decodeUri(resolver, mediaUri, null, o)
if (o.outMimeType != null) {
mediaType = o.outMimeType
}
val size = Point(o.outWidth, o.outHeight)
o.inSampleSize = Utils.calculateInSampleSize(o.outWidth, o.outHeight,
sizeLimit.width, sizeLimit.height)
o.inJustDecodeBounds = false
if (o.outWidth > 0 && o.outHeight > 0 && mediaType != "image/gif") {
val displayOptions = DisplayImageOptions.Builder()
.considerExifParams(true)
.build()
val bitmap = mediaLoader.loadImageSync(mediaUri.toString(),
ImageSize(o.outWidth, o.outHeight).scaleDown(o.inSampleSize),
displayOptions)
if (bitmap != null) {
size.set(bitmap.width, bitmap.height)
val os = DirectByteArrayOutputStream()
when (mediaType) {
"image/png", "image/x-png", "image/webp", "image-x-webp" -> {
bitmap.compress(Bitmap.CompressFormat.PNG, 0, os)
}
else -> {
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, os)
}
}
length = os.size().toLong()
return MediaStreamData(TypedContentLengthInputStream(os.inputStream(true), length, mediaType), size)
}
}
return null
}
private fun videoStream(
context: Context,
resolver: ContentResolver,
mediaUri: Uri,
defaultType: String?
): MediaStreamData? {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
return null
}
val ext = mediaUri.lastPathSegment.substringAfterLast(".")
val pfd = resolver.openFileDescriptor(mediaUri, "r")
val strategy = MediaFormatStrategyPresets.createAndroid720pStrategy()
val listener = object : MediaTranscoder.Listener {
override fun onTranscodeFailed(exception: Exception?) {
}
override fun onTranscodeCompleted() {
}
override fun onTranscodeProgress(progress: Double) {
}
override fun onTranscodeCanceled() {
}
}
val tempFile = File.createTempFile("twidere__encoded_video_", ".$ext", context.cacheDir)
val future = MediaTranscoder.getInstance().transcodeVideo(pfd.fileDescriptor,
tempFile.absolutePath, strategy, listener)
try {
future.get()
} catch (e: Exception) {
tempFile.delete()
return null
}
return MediaStreamData(TypedContentLengthInputStream(tempFile.inputStream(),
tempFile.length(), defaultType), null)
}
internal class MediaStreamData(
val stream: TypedContentLengthInputStream?,
val geometry: Point?
)
internal class TypedContentLengthInputStream(
st: InputStream,
length: Long,
val type: String?
) : ContentLengthInputStream(st, length)
} }
} }