added video encoding
This commit is contained in:
parent
c34ca2fac6
commit
36fb5f223a
|
@ -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'
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue