You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Kotlin中Firebase Storage视频仅显URL无法播放求助

时间胶囊App视频播放问题解决方案

问题描述

开发一款时间胶囊类App,支持存储文本、图片和视频,内容仅在指定时间展示。目前文本和图片功能正常,但视频消息仅显示URL而非实际播放。已尝试使用VideoView等方法,问题仍未解决,需要实现Android Kotlin中流式播放Firebase Storage视频的功能。

现有Adapter代码

import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.timescape.Messages
import com.example.timescape.R
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.ui.PlayerView
import com.squareup.picasso.Picasso
import java.text.SimpleDateFormat
import java.util.*

class MessageAdapter(private val messages: List<Messages>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    private val TYPE_TEXT = 0
    private val TYPE_IMAGE = 1
    private val TYPE_VIDEO = 2

    override fun getItemViewType(position: Int): Int {
        val message = messages[position]
        return when {
            message.mediaUrls.isNotEmpty() && message.mediaUrls[0].endsWith(".mp4") -> TYPE_VIDEO
            message.mediaUrls.isNotEmpty() -> TYPE_IMAGE
            else -> TYPE_TEXT
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return when (viewType) {
            TYPE_IMAGE -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.item_imgmsg, parent, false)
                ImageViewHolder(view)
            }
            TYPE_VIDEO -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.item_vidmsg, parent, false)
                VideoViewHolder(view)
            }
            else -> {
                val view = LayoutInflater.from(parent.context).inflate(R.layout.item_textmsg, parent, false)
                TextViewHolder(view)
            }
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val message = messages[position]
        val displayDateTime = Date(message.openDateTime)
        val currentDateTime = Date()

        if (currentDateTime.after(displayDateTime)) {
            when (holder) {
                is ImageViewHolder -> {
                    Picasso.get().load(message.mediaUrls[0]).into(holder.imageView)
                    holder.dateTimeView.text = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(message.storedDateTime))
                }
                is VideoViewHolder -> {
                    holder.bind(message.mediaUrls[0])
                    holder.dateTimeView.text = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(message.storedDateTime))
                }
                is TextViewHolder -> {
                    holder.textView.text = message.content
                    holder.dateTimeView.text = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date(message.storedDateTime))
                }
            }
        } else {
            val futureMessageText = "This message will be displayed on ${SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(displayDateTime)}"
            when (holder) {
                is ImageViewHolder -> {
                    holder.imageView.setImageResource(R.drawable.baseline_image_24)
                    holder.dateTimeView.text = futureMessageText
                }
                is VideoViewHolder -> {
                    holder.itemView.setBackgroundResource(R.drawable.baseline_videocam_24)
                    holder.dateTimeView.text = futureMessageText
                }
                is TextViewHolder -> {
                    holder.textView.text = futureMessageText
                }
            }
        }
    }

    override fun getItemCount() = messages.size

    class ImageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val imageView: ImageView = itemView.findViewById(R.id.message_image)
        val dateTimeView: TextView = itemView.findViewById(R.id.message_date_time)
    }

    class VideoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val playerView: PlayerView = itemView.findViewById(R.id.player_view)
        val dateTimeView: TextView = itemView.findViewById(R.id.message_date_time)
        private var exoPlayer: ExoPlayer? = null

        fun bind(videoUrl: String) {
            if (exoPlayer == null) {
                exoPlayer = ExoPlayer.Builder(itemView.context).build()
                playerView.player = exoPlayer
            }

            val mediaItem = MediaItem.fromUri(Uri.parse(videoUrl))
            exoPlayer?.setMediaItem(mediaItem)
            exoPlayer?.prepare()
            exoPlayer?.playWhenReady = true
        }

        fun releasePlayer() {
            exoPlayer?.release()
            exoPlayer = null
        }
    }

    class TextViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val textView: TextView = itemView.findViewById(R.id.message_text)
        val dateTimeView: TextView = itemView.findViewById(R.id.message_date_time)
    }

    override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
        if (holder is VideoViewHolder) {
            holder.releasePlayer()
        }
        super.onViewRecycled(holder)
    }
}

数据类代码

package com.example.timescape

data class Messages(
    val id: String="",
    val content: String="",
    val openDateTime: Long=0L,
    val mediaUrls: List<String> = listOf(),
    var isRead: Boolean = false,
    var type: String = "text",
    val storedDateTime: Long = 0L
)

解决方案

1. 确认Firebase Storage视频URL有效性

Firebase Storage的存储路径(如gs://bucket-name/path/video.mp4)无法直接用于播放,必须获取临时HTTPS下载URL:

val storageRef = FirebaseStorage.getInstance().getReferenceFromUrl("gs://your-bucket-name/video-path.mp4")
storageRef.downloadUrl.addOnSuccessListener { uri ->
    val validDownloadUrl = uri.toString()
    // 将此URL存入Messages的mediaUrls字段
}

检查当前mediaUrls中的值是否为有效的HTTPS URL,而非存储路径或相对路径。

2. 修复VideoViewHolder绑定逻辑

优化播放器复用逻辑,避免状态残留:

fun bind(videoUrl: String) {
    if (exoPlayer == null) {
        exoPlayer = ExoPlayer.Builder(itemView.context).build()
        playerView.player = exoPlayer
    } else {
        exoPlayer?.stop()
        exoPlayer?.clearMediaItems()
    }

    val mediaItem = MediaItem.fromUri(Uri.parse(videoUrl))
    exoPlayer?.addMediaItem(mediaItem)
    exoPlayer?.prepare()
    exoPlayer?.playWhenReady = true
}

3. 检查视频项布局文件

确保item_vidmsg.xml中PlayerView布局正确,无遮挡且宽高合理:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="8dp">

    <com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/player_view"
        android:layout_width="match_parent"
        android:layout_height="200dp" />

    <TextView
        android:id="@+id/message_date_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:textSize="12sp" />
</LinearLayout>

确认布局中没有额外显示URL的TextView组件。

4. 添加必要权限

在AndroidManifest.xml中添加网络权限:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- 可选,用于检测网络状态 -->

5. 优化未到展示时间的处理逻辑

在onBindViewHolder的else分支中,确保释放播放器并清除状态:

is VideoViewHolder -> {
    holder.releasePlayer()
    holder.playerView.setDefaultArtwork(null)
    holder.itemView.setBackgroundResource(R.drawable.baseline_videocam_24)
    holder.dateTimeView.text = futureMessageText
}

内容的提问来源于stack exchange,提问作者Muskan Bandri

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 17:25:56