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

Android应用开发求助:将Python服务器JPEG流推送至VideoView

解决Android VideoView播放Python服务器JPEG流的问题

嘿,我来帮你搞定这个直播展示的问题!首先得说清楚:原生VideoView本身并不支持直接播放连续的JPEG图片流,它主要是为标准视频格式(比如H.264编码的MP4)设计的。不过咱们有两种靠谱的思路来实现你的需求,下面一步步给你讲:

方案一:用SurfaceView逐帧渲染JPEG流(最直接的方案)

这个思路是直接从Python服务器拉取JPEG帧,然后在SurfaceView上逐帧绘制,模拟直播效果。

步骤1:Android端实现SurfaceView渲染

首先创建一个自定义的SurfaceView,用来处理帧的绘制:

class JpegStreamView(context: Context, attrs: AttributeSet?) : SurfaceView(context, attrs), SurfaceHolder.Callback {
    private var holder: SurfaceHolder = holder.apply { addCallback(this@JpegStreamView) }
    private var streamThread: Thread? = null
    private var isRunning = false

    override fun surfaceCreated(holder: SurfaceHolder) {
        isRunning = true
        streamThread = Thread { fetchAndRenderFrames() }
        streamThread?.start()
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        isRunning = false
        streamThread?.join()
    }

    private fun fetchAndRenderFrames() {
        val socket = Socket("你的Python服务器IP", 8080) // 替换成你的服务器地址和端口
        val inputStream = socket.getInputStream()
        val buffer = ByteArray(1024 * 1024) // 1MB缓冲区,根据你的帧大小调整

        while (isRunning) {
            // 读取JPEG帧:JPEG以0xFFD8开头,0xFFD9结尾
            var bytesRead = 0
            var startIndex = -1
            var endIndex = -1

            // 先找到帧的起始标记
            while (startIndex == -1 && bytesRead < buffer.size) {
                val read = inputStream.read(buffer, bytesRead, buffer.size - bytesRead)
                if (read == -1) break
                bytesRead += read
                for (i in 0 until bytesRead - 1) {
                    if (buffer[i] == 0xFF.toByte() && buffer[i+1] == 0xD8.toByte()) {
                        startIndex = i
                        break
                    }
                }
            }

            // 再找到帧的结束标记
            while (endIndex == -1 && bytesRead < buffer.size) {
                val read = inputStream.read(buffer, bytesRead, buffer.size - bytesRead)
                if (read == -1) break
                bytesRead += read
                for (i in startIndex until bytesRead - 1) {
                    if (buffer[i] == 0xFF.toByte() && buffer[i+1] == 0xD9.toByte()) {
                        endIndex = i + 2
                        break
                    }
                }
            }

            // 解析并绘制Bitmap
            if (startIndex != -1 && endIndex != -1) {
                val frameBytes = buffer.copyOfRange(startIndex, endIndex)
                val bitmap = BitmapFactory.decodeByteArray(frameBytes, 0, frameBytes.size)
                holder.lockCanvas()?.let { canvas ->
                    canvas.drawBitmap(bitmap, 0f, 0f, null)
                    holder.unlockCanvasAndPost(canvas)
                }
                bitmap.recycle() // 释放Bitmap内存,避免OOM

                // 把剩下的未处理字节移到缓冲区开头,用于下一次读取
                val remaining = bytesRead - endIndex
                System.arraycopy(buffer, endIndex, buffer, 0, remaining)
                bytesRead = remaining
                startIndex = -1
                endIndex = -1
            }
        }

        inputStream.close()
        socket.close()
    }
}

然后在布局里引用这个View:

<com.yourpackage.JpegStreamView
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

步骤2:Python服务器端发送JPEG流

这里给你一个简单的Python示例,用socket持续发送JPEG帧(比如从摄像头采集或者本地图片):

import socket
import cv2
import time

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('0.0.0.0', 8080))
server_socket.listen(5)

print("等待客户端连接...")
client_socket, addr = server_socket.accept()
print(f"客户端已连接:{addr}")

cap = cv2.VideoCapture(0) # 从摄像头采集,也可以替换成读取本地图片循环发送

try:
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        # 把帧转成JPEG格式
        _, jpeg = cv2.imencode('.jpg', frame)
        # 发送JPEG数据
        client_socket.sendall(jpeg.tobytes())
        time.sleep(0.03) # 控制帧率,约30fps
finally:
    cap.release()
    client_socket.close()
    server_socket.close()

方案二:用ExoPlayer播放MJPEG流(更接近VideoView的体验)

如果更想用类似VideoView的组件,推荐用Google的ExoPlayer,它支持MJPEG格式(JPEG流的一种标准封装)。

步骤1:添加ExoPlayer依赖

在app的build.gradle里添加:

implementation "com.google.android.exoplayer:exoplayer-core:2.19.1"
implementation "com.google.android.exoplayer:exoplayer-ui:2.19.1"

步骤2:布局里添加ExoPlayer的View

<com.google.android.exoplayer2.ui.StyledPlayerView
    android:id="@+id/playerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

步骤3:代码里配置播放MJPEG流

class MainActivity : AppCompatActivity() {
    private lateinit var player: ExoPlayer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        player = ExoPlayer.Builder(this).build()
        findViewById<StyledPlayerView>(R.id.playerView).player = player

        // 替换成你的MJPEG流地址(Python服务器需要提供MJPEG格式的流)
        val mediaItem = MediaItem.fromUri("http://你的服务器IP:8080/mjpeg")
        player.setMediaItem(mediaItem)
        player.prepare()
        player.play()
    }

    override fun onDestroy() {
        super.onDestroy()
        player.release()
    }
}

Python服务器提供MJPEG流

用Flask可以快速搭建一个MJPEG服务器:

from flask import Flask, Response
import cv2

app = Flask(__name__)
cap = cv2.VideoCapture(0)

def generate_frames():
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        _, buffer = cv2.imencode('.jpg', frame)
        frame = buffer.tobytes()
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')

@app.route('/mjpeg')
def mjpeg_stream():
    return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080, threaded=True)

常见坑提示

  • 网络操作不能在主线程:上面的代码里已经用了子线程,Android 4.0之后主线程不允许网络请求,一定要注意。
  • 内存优化:逐帧渲染时记得回收Bitmap,避免内存泄漏和OOM。
  • 流的格式兼容:如果用ExoPlayer,服务器必须输出标准的MJPEG流(带multipart边界头),否则ExoPlayer无法识别。

内容的提问来源于stack exchange,提问作者Misunderstood Salad

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.19 03:30:23