提交 cacc3d68 authored 作者: jiangjun's avatar jiangjun

实现android点播播放器截图

上级 6c5fa77a
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -7,6 +7,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
......@@ -15,6 +16,7 @@ import com.tencent.rtmp.ITXVodPlayListener;
import com.tencent.rtmp.TXBitrateItem;
import com.tencent.rtmp.TXImageSprite;
import com.tencent.rtmp.TXLiveConstants;
import com.tencent.rtmp.TXLivePlayer;
import com.tencent.rtmp.TXPlayInfoParams;
import com.tencent.rtmp.TXPlayerDrmBuilder;
import com.tencent.rtmp.TXTrackInfo;
......@@ -23,6 +25,7 @@ import com.tencent.rtmp.TXVodDef;
import com.tencent.rtmp.TXVodPlayConfig;
import com.tencent.rtmp.TXVodPlayer;
import com.tencent.rtmp.ui.TXCloudVideoView;
import com.tencent.ugc.TXRecordCommon;
import com.tencent.vod.flutter.FTXEvent;
import com.tencent.vod.flutter.FTXPIPManager;
import com.tencent.vod.flutter.FTXTransformation;
......@@ -52,6 +55,9 @@ import com.tencent.vod.flutter.ui.render.FTXRenderView;
import com.tencent.vod.flutter.ui.render.FTXRenderViewFactory;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
......@@ -74,6 +80,7 @@ public class FTXVodPlayer extends FTXVodPlayerRenderHost implements ITXVodPlayLi
private TXVodPlayer mVodPlayer;
private TXImageSprite mTxImageSprite;
private FTXRenderView mRenderView;
private static final int Uninitialized = -101;
private boolean mEnableHardwareDecode = true;
......@@ -272,6 +279,56 @@ public class FTXVodPlayer extends FTXVodPlayerRenderHost implements ITXVodPlayLi
}
}
@NonNull
@Override
public FtxMessages.SnapshotRespMsg snapshot(@NonNull FtxMessages.SnapshotReqMsg playerMsg) {
// mVodPlayer.snapshot(bitmap -> {
//
// });
if (mVodPlayer != null && mRenderView != null) {
final FtxMessages.SnapshotRespMsg.Builder respBuilder = new FtxMessages.SnapshotRespMsg.Builder();
final Bitmap bitmap = mRenderView.captureBitmap();
if (bitmap != null && playerMsg.getFilePath() != null) {
// 确定图片格式
Bitmap.CompressFormat format = (playerMsg.getFormat() != null && playerMsg.getFormat() == 1)
? Bitmap.CompressFormat.JPEG
: Bitmap.CompressFormat.PNG;
// 确定图片质量
int quality = 100;
if (playerMsg.getQuality() != null) {
quality = Math.max(0, Math.min(100, (int) (playerMsg.getQuality() * 100)));
}
// 保存图片
try {
File file = new File(playerMsg.getFilePath());
FileOutputStream out = new FileOutputStream(file);
bitmap.compress(format, quality, out);
out.flush();
out.close();
respBuilder.setSuccess(true);
respBuilder.setReason("");
} catch (IOException e) {
Log.e(TAG, "save bitmap failed", e);
respBuilder.setSuccess(false);
respBuilder.setReason(e.getMessage());
}
} else {
respBuilder.setSuccess(false);
respBuilder.setReason("bitmap is null or file path is null");
}
return respBuilder.build();
}
// 如果播放器为空,返回失败结果
return new FtxMessages.SnapshotRespMsg.Builder()
.setSuccess(false)
.setReason("player is null or renderView is null")
.build();
}
protected long init(boolean onlyAudio) {
if (mVodPlayer == null) {
mVodPlayer = new TXVodPlayer(mFlutterPluginBinding.getApplicationContext());
......@@ -887,6 +944,7 @@ public class FTXVodPlayer extends FTXVodPlayerRenderHost implements ITXVodPlayLi
public void setPlayerView(@NonNull Long renderViewId) {
int viewId = renderViewId.intValue();
FTXRenderView renderView = mRenderViewFactory.findViewById(viewId);
mRenderView = renderView;
if (null == renderView) {
LiteavLog.e(TAG, "setPlayerView can not find renderView by id:"
+ viewId + ", release player's renderView");
......
package com.tencent.vod.flutter.player.render.gl;
import android.graphics.Bitmap;
import android.opengl.GLES20;
import java.nio.IntBuffer;
import android.graphics.SurfaceTexture;
import android.opengl.EGL14;
......@@ -498,4 +501,76 @@ public class FTXEGLRender implements SurfaceTexture.OnFrameAvailableListener {
}
}
/**
* 捕获当前渲染帧的图像
* @return Bitmap 截图结果,如果失败则返回null
*/
public Bitmap captureFrame() {
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY || mEGLSurfaceEncoder == EGL14.EGL_NO_SURFACE) {
LiteavLog.e(TAG, "Cannot capture frame, EGL not initialized");
return null;
}
try {
// 保存当前EGL环境
saveCurrentEglEnvironment();
// 设置当前EGL上下文
if (!makeCurrent(1)) {
LiteavLog.e(TAG, "Failed to make current context");
restoreEglEnvironment();
return null;
}
// 读取像素数据
int width = mViewWidth > 0 ? mViewWidth : mWidth;
int height = mViewHeight > 0 ? mViewHeight : mHeight;
if (width <= 0 || height <= 0) {
LiteavLog.e(TAG, "Invalid dimensions for capture: " + width + "x" + height);
restoreEglEnvironment();
return null;
}
// 创建缓冲区存储像素数据
int[] pixels = new int[width * height];
IntBuffer pixelBuffer = IntBuffer.wrap(pixels);
// 从OpenGL ES读取像素
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixelBuffer);
// 检查是否有OpenGL错误
int error = GLES20.glGetError();
if (error != GLES20.GL_NO_ERROR) {
LiteavLog.e(TAG, "OpenGL error during glReadPixels: " + error);
restoreEglEnvironment();
return null;
}
// 恢复之前的EGL环境
restoreEglEnvironment();
// 创建Bitmap并转换像素格式(OpenGL坐标系与Bitmap坐标系不同,需要垂直翻转)
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
IntBuffer flippedBuffer = IntBuffer.allocate(width * height);
// 垂直翻转图像数据
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int srcIndex = (height - 1 - y) * width + x;
flippedBuffer.put(pixels[srcIndex]);
}
}
flippedBuffer.rewind();
bitmap.copyPixelsFromBuffer(flippedBuffer);
return bitmap;
} catch (Exception e) {
LiteavLog.e(TAG, "Error capturing frame", e);
restoreEglEnvironment();
return null;
}
}
}
package com.tencent.vod.flutter.ui.render;
import android.graphics.Bitmap;
import com.tencent.vod.flutter.player.render.FTXPlayerRenderSurfaceHost;
public interface FTXRenderCarrier {
......@@ -27,4 +29,6 @@ public interface FTXRenderCarrier {
void removeSurfaceTextureListener(FTXCarrierSurfaceListener listener);
void removeAllSurfaceListener();
Bitmap captureBitmap();
}
package com.tencent.vod.flutter.ui.render;
import android.content.Context;
import android.graphics.Bitmap;
import android.view.View;
import android.view.ViewGroup;
......@@ -103,4 +104,16 @@ public class FTXRenderView implements PlatformView {
mContainer.setCarrier(null);
LiteavLog.i(TAG, "render view is dispose, id:" + mViewId + ", view:" + hashCode());
}
/**
* 截取当前纹理视图的图像
* @return Bitmap 截图结果,如果失败则返回null
*/
public Bitmap captureBitmap() {
if (mTextureView != null) {
return mTextureView.captureBitmap();
}
return null;
}
}
package com.tencent.vod.flutter.ui.render;
import android.content.Context;
import android.graphics.Bitmap;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
......@@ -227,6 +228,14 @@ public class FTXSurfaceView extends SurfaceView implements FTXRenderCarrier {
mSurfaceListenerDelegate.mExternalSurfaceListeners.clear();
}
@Override
public Bitmap captureBitmap() {
if (mRender != null) {
return mRender.captureFrame();
}
return null;
}
private static class SurfaceViewInnerListener implements SurfaceHolder.Callback {
private final List<FTXCarrierSurfaceListener> mExternalSurfaceListeners = new CopyOnWriteArrayList<>();
......
package com.tencent.vod.flutter.ui.render;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.SurfaceTexture;
import android.view.Surface;
import android.view.TextureView;
......@@ -284,4 +285,13 @@ public class FTXTextureView extends TextureView implements FTXRenderCarrier {
}
}
@Override
public Bitmap captureBitmap() {
if (mRender != null) {
return mRender.captureFrame();
}
return null;
}
}
......@@ -27,7 +27,7 @@ android {
namespace="com.example.flutter.player"
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.tencent.liteav.demo"
applicationId "com.hiteacher.industrylearner2"
minSdkVersion flutter.minSdkVersion
targetSdk 34
versionCode flutterVersionCode.toInteger()
......
......@@ -4,8 +4,8 @@
import 'dart:async';
// The obtained license URL
const LICENSE_URL = "";
const LICENSE_URL = "https://1390221090.trtcube-license.cn/license/v2/1390221090_1/v_cube.license";
// The obtained license key
const LICENSE_KEY = "";
const LICENSE_KEY = "ac0e94a28975a987b86c29429275ecf5";
Completer<bool> isLicenseSuc = Completer();
\ No newline at end of file
......@@ -320,6 +320,8 @@ class _DemoSuperPlayerState extends State<DemoSuperPlayer> with TXPipPlayerResto
SuperPlayerModel model = SuperPlayerModel();
model.title = AppLocals.current.playerTestVideo;
model.videoURL = "http://liteavapp.qcloud.com/live/liteavdemoplayerstreamid_demo1080p.flv";
// model.videoURL = 'https://cdn.pixabay.com/video/2025/10/22/311442_large.mp4';
// model.videoURL = 'https://1257307760.vod2.myqcloud.com/ce465e88vodcq1257307760/df8e0dfa1253642701442822691/Km2GPDojwSEA.mp4';
model.coverUrl =
"http://1500005830.vod2.myqcloud.com/6c9a5118vodcq1500005830/66bc542f387702300661648850/0RyP1rZfkdQA.png";
model.playAction = playAction;
......
......@@ -37,7 +37,8 @@ class _MyAppState extends State<MyApp> {
initPlayerLicense();
initPlatformState();
_getFlutterSdkVersion();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
SystemChrome.setPreferredOrientations([]);
LogUtils.logOpen = true;
}
......
......@@ -33,6 +33,8 @@ dependencies:
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
path_provider: 2.1.5
dependency_overrides:
# force override flutter_easyloading dep
flutter_spinkit: 5.1.0
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论