diff --git a/app/build.gradle b/app/build.gradle index fe8f042..a8cd1c4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -76,6 +76,10 @@ dependencies { implementation 'com.github.chrisbanes:PhotoView:latest_version' implementation 'com.github.chrisbanes:PhotoView:2.1.4' + implementation 'com.google.android.exoplayer:exoplayer-core:2.19.0' + implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.0' + implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.0' + implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' diff --git a/app/src/main/java/org/zus/helloworld/ui/vult/VultFragment.kt b/app/src/main/java/org/zus/helloworld/ui/vult/VultFragment.kt index 92eae34..6691f35 100644 --- a/app/src/main/java/org/zus/helloworld/ui/vult/VultFragment.kt +++ b/app/src/main/java/org/zus/helloworld/ui/vult/VultFragment.kt @@ -2,10 +2,7 @@ package org.zus.helloworld.ui.vult import android.app.Activity import android.app.Dialog -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent +import android.content.* import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.net.Uri @@ -33,6 +30,19 @@ import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.github.barteksc.pdfviewer.PDFView import com.github.chrisbanes.photoview.PhotoView +import com.google.android.exoplayer2.C +import com.google.android.exoplayer2.ExoPlayer +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.source.MediaSource +import com.google.android.exoplayer2.source.ProgressiveMediaSource +import com.google.android.exoplayer2.ui.StyledPlayerView +import com.google.android.exoplayer2.ui.TimeBar +import com.google.android.exoplayer2.ui.TimeBar.OnScrubListener +import com.google.android.exoplayer2.upstream.DataSource +import com.google.android.exoplayer2.upstream.DataSpec +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory +import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException import com.google.android.material.snackbar.Snackbar import com.google.gson.Gson import com.google.gson.JsonArray @@ -47,6 +57,9 @@ import org.zus.helloworld.utils.Utils.Companion.getConvertedDateTime import org.zus.helloworld.utils.Utils.Companion.getConvertedSize import org.zus.helloworld.utils.Utils.Companion.getSizeInB import org.zus.helloworld.utils.Utils.Companion.getTimeInNanoSeconds +import org.zus.helloworld.utils.streaming.ZChainDataSource +import org.zus.helloworld.utils.streaming.ZChainLocalFile +import org.zus.helloworld.utils.streaming.ZChainMediaSourceFactory import zbox.StatusCallbackMocked import zcncore.Zcncore import java.io.File @@ -66,7 +79,8 @@ val LOCK_TOKENS: String = Zcncore.convertToValue(1.0) private const val REQUEST_FILES = 1 private const val RESULT_ERROR = 64 -class VultFragment : Fragment(), FileClickListener, ThumbnailDownloadCallback { +class VultFragment : Fragment(), FileClickListener, ThumbnailDownloadCallback, + DialogInterface.OnDismissListener { private lateinit var binding: VultFragmentBinding private lateinit var vultViewModel: VultViewModel private lateinit var mainViewModel: MainViewModel @@ -78,6 +92,8 @@ class VultFragment : Fragment(), FileClickListener, ThumbnailDownloadCallback { private var currentFilePosition = -1 private var filesAdapter: FilesAdapter? = null private var currentSnackbar: Snackbar? = null + var progressLayout: View? = null + var exoPlayer: ExoPlayer? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -164,6 +180,8 @@ class VultFragment : Fragment(), FileClickListener, ThumbnailDownloadCallback { previewLayout = layoutInflater.inflate(R.layout.dialog_file_preview, null) previewDialog!!.setContentView(previewLayout!!) previewDialog!!.setCancelable(true) + previewDialog!!.setOnDismissListener(this) + progressLayout = previewLayout!!.findViewById(R.id.progressLayout) val documentPicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> @@ -584,7 +602,8 @@ class VultFragment : Fragment(), FileClickListener, ThumbnailDownloadCallback { ) { CoroutineScope(Dispatchers.Main).launch { isRefresh(false) - updateFilePreview(filePosition, files) + if(!files.mimeType!!.startsWith("video/")) + updateFilePreview(filePosition, files) } } @@ -663,9 +682,13 @@ class VultFragment : Fragment(), FileClickListener, ThumbnailDownloadCallback { val titleText = previewLayout!!.findViewById(R.id.fileName) titleText.text = files.name val btnNext = previewLayout!!.findViewById(R.id.btnNext) - btnNext.setOnClickListener { previewAction(position + 1, true) } + btnNext.setOnClickListener { + releasePlayer() + previewAction(position + 1, true) } val btnPrevious = previewLayout!!.findViewById(R.id.btnPrevious) - btnPrevious.setOnClickListener { previewAction(position - 1, false) } + btnPrevious.setOnClickListener { + releasePlayer() + previewAction(position - 1, false) } // Dismiss the dialog when the back button is clicked val backButton = previewLayout!!.findViewById(R.id.headerBack) backButton.setOnClickListener { previewDialog!!.dismiss() } @@ -675,54 +698,61 @@ class VultFragment : Fragment(), FileClickListener, ThumbnailDownloadCallback { private fun openFile(position: Int, files: Files) { currentFilePosition = position var file: File? = null - if (files.getAndroidPath() != null) file = File(files.getAndroidPath()) + file = files.getAndroidPath()?.let { File(it) } val mimeType: String? = files.mimeType showPreviewDialog(position, files) if (file == null || !file.exists()) { + val photoPreview = previewLayout!!.findViewById(R.id.filePreview) + photoPreview.visibility = View.VISIBLE + val videoPlayerView: StyledPlayerView = + previewLayout!!.findViewById(R.id.videoPlayerView) + videoPlayerView.visibility = View.INVISIBLE if (mimeType?.startsWith("image/") == true || mimeType?.startsWith("video/") == true) { val thumbnailPath: String? = files.thumbnailPath if (thumbnailPath != null) { val thumbnailFile: File? = files.thumbnailPath?.let { File(it) } - val filePreview: PhotoView = previewLayout?.findViewById(R.id.filePreview)!! - if (thumbnailFile?.exists() == true) filePreview.setImageURI( + if (thumbnailFile?.exists() == true) photoPreview.setImageURI( Uri.fromFile( thumbnailFile ) ) } } else if (mimeType.equals("application/pdf")) { - val filePreview: PhotoView = previewLayout?.findViewById(R.id.filePreview)!! - filePreview.setImageResource(R.drawable.ic_upload_document) + photoPreview.setImageResource(R.drawable.ic_upload_document) } - val loadingText = previewLayout?.findViewById(R.id.loadingText)!! - loadingText.visibility = View.VISIBLE - return } } private fun updateFilePreview(position: Int, file: Files) { - val actualFile = File(file.getAndroidPath()) - if (currentFilePosition == position && actualFile.exists()) { - val loadingText = previewLayout?.findViewById(R.id.loadingText)!! - loadingText.visibility = View.GONE - val mimeType: String? = file.mimeType - val photoPreview: PhotoView = previewLayout!!.findViewById(R.id.filePreview) - val pdfPreview: PDFView = previewLayout!!.findViewById(R.id.pdfPreview) - if (mimeType != null && mimeType.startsWith("image/")) { - photoPreview.visibility = View.VISIBLE + val mimeType: String = file.mimeType!! + if (currentFilePosition == position) { + val photoPreview = previewLayout!!.findViewById(R.id.filePreview) + val pdfPreview = previewLayout!!.findViewById(R.id.pdfPreview) + val videoPlayerView = + previewLayout!!.findViewById(R.id.videoPlayerView) + if (mimeType.startsWith("image/")) { pdfPreview.visibility = View.INVISIBLE + photoPreview.visibility = View.VISIBLE + videoPlayerView.visibility = View.INVISIBLE + progressLayout!!.visibility = View.INVISIBLE + val actualFile = file.getAndroidPath()?.let { File(it) } photoPreview.setImageURI(Uri.fromFile(actualFile)) - } else if (mimeType.equals("application/pdf")) { + } else if (mimeType.startsWith("video/")) { + pdfPreview.visibility = View.INVISIBLE + showVideoPreview(position, file) + }else if (mimeType == "application/pdf") { pdfPreview.visibility = View.VISIBLE photoPreview.visibility = View.INVISIBLE + videoPlayerView.visibility = View.INVISIBLE + progressLayout!!.visibility = View.INVISIBLE try { + val actualFile = file.getAndroidPath()?.let { File(it) } pdfPreview.fromFile(actualFile).load() } catch (e: Exception) { e.message?.let { Log.e(TAG_VULT, it) } } } else { - var f: File? = null - if (file.getAndroidPath() != null) f = actualFile + val f = file.getAndroidPath()?.let { File(it) } val fileUri = f?.let { FileProvider.getUriForFile( requireActivity(), @@ -766,10 +796,105 @@ class VultFragment : Fragment(), FileClickListener, ThumbnailDownloadCallback { } } + private fun showVideoPreview(position: Int, file: Files) { + val localFile = file.getAndroidPath()?.let { File(it) } + var mediaSource: MediaSource + val item: MediaItem + var zChainMediaSourceFactory: ZChainMediaSourceFactory? = null + var tmpFile: File? = null + if (localFile?.exists() == true) { + item = MediaItem.fromUri(Uri.fromFile(localFile)) + val dataSourceFactory: DataSource.Factory = DefaultDataSourceFactory( + requireActivity(), "user-agent" + ) + mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(item) + play(mediaSource, file, null, null) + } else { + val zChainLocalFile = ZChainLocalFile(file.remotePath) + zChainLocalFile.tempPath= requireActivity().cacheDir.path + zChainLocalFile.fileTotalBytes = file.actualFileSize + zChainLocalFile.numBlocks = file.numBlocks.toLong() + zChainLocalFile.lookupHash = file.getLookupHash() + zChainLocalFile.chunkSize = 65536L + tmpFile = File(requireActivity().cacheDir, "media.mp4") + if (tmpFile.exists()) { + tmpFile.delete() + } + item = MediaItem.Builder() + .setUri(Uri.parse(tmpFile.absolutePath)) + .build() + CoroutineScope(Dispatchers.Main).launch { + withContext(Dispatchers.Main){ + zChainMediaSourceFactory = ZChainMediaSourceFactory( + ZChainDataSource.Factory( + vultViewModel.getAllocation()!!, + zChainLocalFile + ) + ) + mediaSource = zChainMediaSourceFactory!!.createMediaSource(item) + play(mediaSource, file, zChainMediaSourceFactory, tmpFile) + } + } + } + } + private fun play( + mediaSource: MediaSource, + file: Files, + zChainMediaSourceFactory: ZChainMediaSourceFactory?, + tmpFile: File? + ) { + val dataShards: Int = 2 + exoPlayer = ExoPlayer.Builder(requireActivity()).build() + val videoPlayerView = previewLayout!!.findViewById(R.id.videoPlayerView) + videoPlayerView.player = exoPlayer + exoPlayer!!.setMediaSource(mediaSource, true) + exoPlayer!!.prepare() + exoPlayer!!.playWhenReady = true + videoPlayerView.visibility = View.VISIBLE + exoPlayer!!.addListener(object : Player.Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + previewLayout!!.findViewById(R.id.filePreview).visibility = View.INVISIBLE + if (playbackState == Player.STATE_READY || playbackState == Player.STATE_ENDED) { + progressLayout!!.visibility = View.INVISIBLE + } else { + previewLayout!!.visibility = View.VISIBLE + } + } + }) + val scrubListener: OnScrubListener = object : OnScrubListener { + override fun onScrubStart(timeBar: TimeBar, position: Long) { + // Called when user starts interacting with the seek bar + } + + override fun onScrubMove(timeBar: TimeBar, position: Long) {} + override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { + if (zChainMediaSourceFactory != null) { + val effectivePerShardSize: Long = + (file.actualFileSize + dataShards - 1) / dataShards + val effectiveBlockSize = 65536L + val totalFileBlocks = + (effectivePerShardSize + effectiveBlockSize - 1) / effectiveBlockSize + val currentBlock = position * totalFileBlocks / exoPlayer!!.duration + val dataSpec = DataSpec.Builder() + .setUri(Uri.parse(tmpFile!!.absolutePath)) + .setPosition(currentBlock) + .setLength(C.LENGTH_UNSET.toLong()) + .build() + try { + zChainMediaSourceFactory.setDataSpec(dataSpec) + } catch (e: HttpDataSourceException) { + e.printStackTrace() + } + } + } + } + val timeBar: TimeBar = videoPlayerView.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress) + timeBar.addListener(scrubListener) + } override fun onFileClick(filePosition: Int) { val selectedFile = vultViewModel.filesList.value!![filePosition] viewFileAction(filePosition, selectedFile) - } override fun onDownloadFileClickListener(filePosition: Int) { @@ -901,6 +1026,9 @@ class VultFragment : Fragment(), FileClickListener, ThumbnailDownloadCallback { updateFilePreview(filePosition, selectedFile) return } + if (selectedFile.mimeType!!.startsWith("video/")) { + updateFilePreview(filePosition, selectedFile) + } onDownloadToOpenFileClickListener(filePosition) } @@ -1051,4 +1179,12 @@ class VultFragment : Fragment(), FileClickListener, ThumbnailDownloadCallback { Snackbar.make(binding.root, "Successfully Uploaded the File.", Snackbar.LENGTH_SHORT) currentSnackbar!!.show() } + + override fun onDismiss(p0: DialogInterface?) { + releasePlayer() + } + + private fun releasePlayer() { + exoPlayer?.release() + } } diff --git a/app/src/main/java/org/zus/helloworld/utils/streaming/DownloadCallback.kt b/app/src/main/java/org/zus/helloworld/utils/streaming/DownloadCallback.kt new file mode 100644 index 0000000..0a89a17 --- /dev/null +++ b/app/src/main/java/org/zus/helloworld/utils/streaming/DownloadCallback.kt @@ -0,0 +1,7 @@ +package org.zus.helloworld.utils.streaming + +internal abstract class DownloadCallback { + abstract fun completed(data: ByteArray?) + abstract fun inProgress(data: ByteArray?) + abstract fun error(e: Exception?) +} \ No newline at end of file diff --git a/app/src/main/java/org/zus/helloworld/utils/streaming/ZBoxCallback.kt b/app/src/main/java/org/zus/helloworld/utils/streaming/ZBoxCallback.kt new file mode 100644 index 0000000..db76c0f --- /dev/null +++ b/app/src/main/java/org/zus/helloworld/utils/streaming/ZBoxCallback.kt @@ -0,0 +1,62 @@ +package org.zus.helloworld.utils.streaming + +import com.google.android.exoplayer2.util.Log +import zbox.StatusCallbackMocked +import java.io.* + + +class ZBoxCallback internal constructor(private val localPath: String, callback: DownloadCallback) : + StatusCallbackMocked { + private val callback: DownloadCallback? + + init { + this.callback = callback + } + + override fun commitMetaCompleted(s: String, s1: String, e: Exception) {} + override fun completed( + allocationId: String, + filePath: String, + filename: String, + mimetype: String, + size: Long, + op: Long + ) { + val file = File(localPath) + val bytes = ByteArray(file.length().toInt()) + try { + val buf = BufferedInputStream(FileInputStream(file)) + buf.read(bytes, 0, bytes.size) + buf.close() + } catch (e: FileNotFoundException) { + e.printStackTrace() + } catch (e: IOException) { + e.printStackTrace() + } + + // delete file + file.delete() + Log.d("debug", "completed " + bytes.size) + if (callback != null) { + callback.completed(bytes) + } + } + + override fun error(s: String, s1: String, l: Long, e: Exception) { + Log.e("debug", "error", e) + if (callback != null) { + callback.error(e) + } + } + + override fun inProgress(s: String, s1: String, l: Long, l1: Long, data: ByteArray) { + if (callback != null) { + callback.inProgress(data) + } + } + + override fun repairCompleted(l: Long) {} + override fun started(s: String, s1: String, l: Long, l1: Long) { + Log.d("debug", "started $s") + } +} \ No newline at end of file diff --git a/app/src/main/java/org/zus/helloworld/utils/streaming/ZChainDataSource.kt b/app/src/main/java/org/zus/helloworld/utils/streaming/ZChainDataSource.kt new file mode 100644 index 0000000..c067fb9 --- /dev/null +++ b/app/src/main/java/org/zus/helloworld/utils/streaming/ZChainDataSource.kt @@ -0,0 +1,349 @@ +package org.zus.helloworld.utils.streaming + +import android.net.Uri +import com.google.android.exoplayer2.C +import com.google.android.exoplayer2.upstream.BaseDataSource +import com.google.android.exoplayer2.upstream.DataSpec +import com.google.android.exoplayer2.upstream.HttpDataSource +import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException +import com.google.android.exoplayer2.upstream.HttpDataSource.RequestProperties +import com.google.android.exoplayer2.upstream.TransferListener +import com.google.android.exoplayer2.util.Assertions +import com.google.android.exoplayer2.util.Log +import com.google.android.exoplayer2.util.Util +import zbox.Allocation +import java.io.EOFException +import java.io.IOException +import java.util.* +import java.util.concurrent.CountDownLatch +import kotlin.math.max + +class ZChainDataSource private constructor( + allocation: Allocation, + file: ZChainFile +) : BaseDataSource( /* isNetwork= */false), HttpDataSource { + private val requestProperties: RequestProperties + var writePosition = 0 + private var dataSpec: DataSpec? = null + private val skipBuffer: ByteArray? = null + private val opened = false + private var responseCode = 0 + private val bytesToSkip: Long = 0 + private var bytesToRead: Long = 0 + private var bytesSkipped: Long = 0 + private var bytesRead: Long = 0 + private val allocation: Allocation + private var dataRequestStarted = false + private var currentBlockRead: Long = 0 + private val numBlocksToDownload = 5 + private val doneSignal = CountDownLatch(1) + private var totalFileBlocks: Long = 0 + private val file: ZChainFile + private var fileRemotePath = "" + private var streamingData: ByteArray? = ByteArray(INITIAL_STREAMING_DATA_SIZE) + private var readPosition = 0 + private var bytesRemaining = 0 + private val dataShards = 2 + private val DEFAULT_CHUNK_SIZE = 65536L + + init { + requestProperties = RequestProperties() + this.allocation = allocation + this.file = file + fileRemotePath = (file as ZChainLocalFile).fileRemotePath + + } + + private fun startDataRequest(dataSpec: DataSpec) { + dataRequestStarted = true + if (file.numBlocks!! > 0) { + if (totalFileBlocks == 0L) { + val effectivePerShardSize: Long = + (file.fileTotalBytes!! + dataShards - 1) / dataShards + val effectiveBlockSize = + if (file.chunkSize == 0L) DEFAULT_CHUNK_SIZE else file.chunkSize + totalFileBlocks = + (effectivePerShardSize + effectiveBlockSize - 1) / effectiveBlockSize + } + if (dataSpec.position != 0L) { + proceedBlockRead(dataSpec.position) + } else { + val INITIAL_BLOCK = 1 + proceedBlockRead(INITIAL_BLOCK.toLong()) + } + } + } + + fun proceedBlockRead(blockNumber: Long) { + currentBlockRead = blockNumber + val blockSize = if (file.chunkSize == 0L) DEFAULT_CHUNK_SIZE else file.chunkSize + val startIndex = ((blockNumber - 1) * blockSize).toInt() * dataShards + val localPath: String = + file.tempPath + "/" + hashCode() + fileRemotePath + ".ch-" + currentBlockRead + Log.d("debug", "Proceed read $localPath") + val callback = ZBoxCallback(localPath, object : DownloadCallback() { + override fun completed(data: ByteArray?) { + if (data == null || data.size == 0) { + responseCode = 500 + Log.d("debug", "No data in response") + return + } + responseCode = 200 + Log.d( + "debug", + "currentBlockRead $currentBlockRead num $totalFileBlocks" + ) + if (currentBlockRead < totalFileBlocks) { + proceedBlockRead(currentBlockRead + 1) + } + } + + override fun inProgress(data: ByteArray?) { + if (data != null && data.size > 0) { + responseCode = 200 + val endIndex = startIndex + data.size + if (endIndex > streamingData!!.size) { + val expandedData = ByteArray(endIndex) + System.arraycopy(streamingData, 0, expandedData, 0, writePosition) + streamingData = expandedData + } + System.arraycopy(data, 0, streamingData, startIndex, data.size) + writePosition = endIndex + if (writePosition >= readPosition) doneSignal.countDown() + } + } + + override fun error(e: Exception?) { + responseCode = 500 + e!!.printStackTrace() + doneSignal.countDown() + } + }) + val startBlock = currentBlockRead + val endBlock = Math.min(totalFileBlocks, startBlock + numBlocksToDownload) + currentBlockRead = endBlock + transferStarted(dataSpec!!) + if (endBlock >= startBlock) { + try { + Log.i( + "BLOCK_DOWNLOAD", + "Downloading blocks from $startBlock to $endBlock" + ) + val BLOCKS_PE_TRANSACTION = 10 + allocation.downloadFileByBlock( + fileRemotePath, + localPath, + startBlock, + endBlock, + BLOCKS_PE_TRANSACTION.toLong(), + callback, + true + ) + } catch (e: Exception) { + e.printStackTrace() + } + } else { + Log.d( + "debug", + "finished download total: $totalFileBlocks downloaded $currentBlockRead" + ) + } + } + + override fun getUri(): Uri? { + return Uri.parse("@0chain://$fileRemotePath") + } + + override fun getResponseCode(): Int { + return 200 + } + + override fun getResponseHeaders(): Map> { + val fakeHeaders: MutableMap> = HashMap() + fakeHeaders["Content-Type"] = listOf(file.mimeType) + fakeHeaders["Content-Length"] = + listOf(java.lang.String.valueOf(file.fileTotalBytes)) + fakeHeaders["X-Android-Response-Source"] = listOf("NETWORK 200") + fakeHeaders["X-Android-Selected-Protocol"] = listOf("http/1.1") + return fakeHeaders + } + + override fun setRequestProperty(name: String, value: String) { + Assertions.checkNotNull(name) + Assertions.checkNotNull(value) + requestProperties[name] = value + } + + override fun clearRequestProperty(name: String) { + Assertions.checkNotNull(name) + requestProperties.remove(name) + } + + override fun clearAllRequestProperties() { + requestProperties.clear() + } + + /** + * Opens the source to read the specified data. + */ + @Throws(HttpDataSourceException::class) + override fun open(dataSpec: DataSpec): Long { + this.dataSpec = dataSpec + bytesRead = 0 + bytesSkipped = 0 + transferInitializing(dataSpec) + bytesToRead = file.fileTotalBytes!! + if (streamingData == null || streamingData!!.size < bytesToRead) { + val initialSize = Math.max(bytesToRead.toInt(), INITIAL_STREAMING_DATA_SIZE) + streamingData = ByteArray(initialSize) + } + transferStarted(dataSpec) + if (!dataRequestStarted) { + Log.d("debug", " start wait $bytesToRead") + startDataRequest(dataSpec) + try { + doneSignal.await() + } catch (e: InterruptedException) { + e.printStackTrace() + } + Log.d("debug", " continue $bytesToRead") + } + readPosition = dataSpec.position.toInt() + bytesRemaining = + (if (dataSpec.length == C.LENGTH_UNSET.toLong()) writePosition - dataSpec.position else readPosition).toInt() + Log.i( + "debug", + "open - bytesRemaining " + bytesRemaining + " dataSpec.length " + dataSpec.length + " dataSpec.position " + dataSpec.position + ) + return if (bytesRemaining <= 0) C.LENGTH_UNSET.toLong() else bytesRemaining.toLong() + } + + @Throws(HttpDataSourceException::class) + override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int { + return try { + readInternal(buffer, offset, readLength) + } catch (e: IOException) { + throw HttpDataSourceException( + e, Util.castNonNull(dataSpec), HttpDataSourceException.TYPE_READ + ) + } + } + + @Throws(HttpDataSourceException::class) + override fun close() { + Log.i("debug", " ===> closing <===") + } + + /** + * Reads up to `length` bytes of data and stores them into `buffer`, starting at + * index `offset`. + * + * + * This method blocks until at least one byte of data can be read, the end of the opened range is + * detected, or an exception is thrown. + * + * @param buffer The buffer into which the read data should be stored. + * @param offset The start offset into `buffer` at which data should be written. + * @param readLength The maximum number of bytes to read. + * @return The number of bytes read, or [C.RESULT_END_OF_INPUT] if the end of the opened + * range is reached. + * @throws IOException If an error occurs reading from the source. + */ + @Throws(IOException::class) + private fun readInternal(buffer: ByteArray, offset: Int, readLength: Int): Int { + var readLength = readLength + if (readLength == 0) { + return 0 + } else if (writePosition == 0 || writePosition < readLength) { + throw EOFException() + } + if (writePosition < readPosition) { + return 0 + } + readLength = Math.min(readLength, writePosition) + Log.i( + "debug", + "==> readPosition $readPosition offset $offset readLength $readLength" + ) + System.arraycopy(streamingData, readPosition, buffer, offset, readLength) + readPosition += readLength + bytesRemaining -= readLength + return readLength + } + + fun openFromCustomPoint(dataSpec: DataSpec) { + this.dataSpec = dataSpec + bytesRead = 0 + bytesSkipped = 0 + transferInitializing(dataSpec) + bytesToRead = file.fileTotalBytes!! + if (streamingData == null || streamingData!!.size < bytesToRead) { + val initialSize = max(bytesToRead.toInt(), INITIAL_STREAMING_DATA_SIZE) + streamingData = ByteArray(initialSize) + } + transferStarted(dataSpec) + if (!dataRequestStarted) { + Log.d("CUSTOM_POINT", " start wait $bytesToRead") + startDataRequest(dataSpec) + Log.d("CUSTOM_POINT", " continue $bytesToRead") + } + readPosition = dataSpec.position.toInt() + bytesRemaining = + (if (dataSpec.length == C.LENGTH_UNSET.toLong()) writePosition - dataSpec.position else readPosition).toInt() + Log.i( + "CUSTOM_POINT", + "open - bytesRemaining " + bytesRemaining + " dataSpec.length " + dataSpec.length + " dataSpec.position " + dataSpec.position + ) + } + + /** + * [ZChainDataSource.Factory] for [ZChainDataSource] instances. + */ + class Factory(allocation: Allocation, file: ZChainFile) : + HttpDataSource.Factory { + private val defaultRequestProperties: RequestProperties + private var transferListener: TransferListener? = null + private val allocation: Allocation + private val file: ZChainFile + + init { + defaultRequestProperties = RequestProperties() + this.allocation = allocation + this.file = file + } + + /** + * Sets the [TransferListener] that will be used. + * + * + * The default is `null`. + * + * @param transferListener The listener that will be used. + * @return This factory. + */ + fun setTransferListener(transferListener: TransferListener?): Factory { + this.transferListener = transferListener + return this + } + + override fun createDataSource(): ZChainDataSource { + val dataSource = ZChainDataSource( + allocation, + file + ) + if (transferListener != null) { + dataSource.addTransferListener(transferListener!!) + } + return dataSource + } + + override fun setDefaultRequestProperties(defaultRequestProperties: Map): HttpDataSource.Factory { + throw UnsupportedOperationException("setDefaultRequestProperties is not supported.") + } + } + + companion object { + private const val TAG = "ZChainDataSource" + private const val INITIAL_STREAMING_DATA_SIZE = 1024 * 1024 // 1MB + } +} diff --git a/app/src/main/java/org/zus/helloworld/utils/streaming/ZChainFile.kt b/app/src/main/java/org/zus/helloworld/utils/streaming/ZChainFile.kt new file mode 100644 index 0000000..22531df --- /dev/null +++ b/app/src/main/java/org/zus/helloworld/utils/streaming/ZChainFile.kt @@ -0,0 +1,10 @@ +package org.zus.helloworld.utils.streaming + +abstract class ZChainFile { + var tempPath: String? = null + var fileTotalBytes: Long? = null + var numBlocks: Long? = null + var mimeType = "video/mp4" + var lookupHash: String? = null + var chunkSize: Long = 0 +} \ No newline at end of file diff --git a/app/src/main/java/org/zus/helloworld/utils/streaming/ZChainLocalFile.kt b/app/src/main/java/org/zus/helloworld/utils/streaming/ZChainLocalFile.kt new file mode 100644 index 0000000..18166a5 --- /dev/null +++ b/app/src/main/java/org/zus/helloworld/utils/streaming/ZChainLocalFile.kt @@ -0,0 +1,3 @@ +package org.zus.helloworld.utils.streaming + +class ZChainLocalFile(val fileRemotePath: String) : ZChainFile() \ No newline at end of file diff --git a/app/src/main/java/org/zus/helloworld/utils/streaming/ZChainMediaSourceFactory.kt b/app/src/main/java/org/zus/helloworld/utils/streaming/ZChainMediaSourceFactory.kt new file mode 100644 index 0000000..0a939bf --- /dev/null +++ b/app/src/main/java/org/zus/helloworld/utils/streaming/ZChainMediaSourceFactory.kt @@ -0,0 +1,46 @@ +package org.zus.helloworld.utils.streaming + +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.drm.DrmSessionManagerProvider +import com.google.android.exoplayer2.source.MediaSource +import com.google.android.exoplayer2.source.MediaSourceFactory +import com.google.android.exoplayer2.source.ProgressiveMediaSource +import com.google.android.exoplayer2.upstream.DataSpec +import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy + +class ZChainMediaSourceFactory(dataSourceFactory: ZChainDataSource.Factory) : + MediaSourceFactory { + private val dataSourceFactory: ZChainDataSource.Factory + private val progressiveMediaSourceFactory: ProgressiveMediaSource.Factory + private var zChainDataSource: ZChainDataSource? = null + + init { + this.dataSourceFactory = dataSourceFactory + progressiveMediaSourceFactory = ProgressiveMediaSource.Factory(dataSourceFactory) + } + + override fun createMediaSource(mediaItem: MediaItem): MediaSource { + zChainDataSource = dataSourceFactory.createDataSource() as ZChainDataSource + return progressiveMediaSourceFactory.createMediaSource(mediaItem) + } + + override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory { + throw UnsupportedOperationException("setDrmSessionManagerProvider is not supported.") + } + + override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory { + throw UnsupportedOperationException("setLoadErrorHandlingPolicy is not supported.") + } + + override fun getSupportedTypes(): IntArray { + return progressiveMediaSourceFactory.supportedTypes + } + + @Throws(HttpDataSourceException::class) + fun setDataSpec(dataSpec: DataSpec?) { + if (dataSpec != null) { + zChainDataSource?.openFromCustomPoint(dataSpec) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_file_preview.xml b/app/src/main/res/layout/dialog_file_preview.xml index 04885f9..110ae28 100644 --- a/app/src/main/res/layout/dialog_file_preview.xml +++ b/app/src/main/res/layout/dialog_file_preview.xml @@ -54,7 +54,45 @@ app:layout_constraintBottom_toTopOf="@id/btnNext" app:layout_constraintTop_toBottomOf="@id/fileName" android:visibility="invisible"/> + + + + + + + + -