您当前的位置: 首页 > 学无止境 > 心得笔记 网站首页心得笔记
21. 修改了视频播放到结尾再次打开会锁死的bug~1
发布时间:2021-06-14 18:12:23编辑:雪饮阅读()
其實之前在實現鎖的時候cpp/FFDemux.cpp中鎖的實現是有問題的,這裏要修復下:
#include "XLog.h"
extern "C"{
#include <libavformat/avformat.h>
}
//分数转为浮点数
static double r2d(AVRational r)
{
return r.num == 0 || r.den == 0 ?0.:(double) r.num/(double)r.den;
}
void FFDemux::Close()
{
mux.lock();
if(ic)
avformat_close_input(&ic);
mux.unlock();
}
//seek 位置 pos 0.0~1.0
bool FFDemux::Seek(double pos)
{
if(pos<0 || pos > 1)
{
XLOGE("Seek value must 0.0~1.0");
return false;
}
bool re = false;
mux.lock();
if(!ic)
{
mux.unlock();
return false;
}
//清理读取的缓冲
avformat_flush(ic);
long long seekPts = 0;
seekPts = ic->streams[videoStream]->duration*pos;
//往后跳转到关键帧
re = av_seek_frame(ic,videoStream,seekPts,AVSEEK_FLAG_FRAME|AVSEEK_FLAG_BACKWARD);
mux.unlock();
return re;
}
//打开文件,或者流媒体 rmtp http rtsp
bool FFDemux::Open(const char *url)
{
XLOGI("Open file %s begin",url);
Close();
mux.lock();
int re = avformat_open_input(&ic,url,0,0);
if(re != 0 )
{
mux.unlock();
char buf[1024] = {0};
av_strerror(re,buf,sizeof(buf));
XLOGE("FFDemux open %s failed!",url);
return false;
}
XLOGI("FFDemux open %s success!",url);
//读取文件信息
re = avformat_find_stream_info(ic,0);
if(re != 0 )
{
mux.unlock();
char buf[1024] = {0};
av_strerror(re,buf,sizeof(buf));
XLOGE("avformat_find_stream_info %s failed!",url);
return false;
}
this->totalMs = ic->duration/(AV_TIME_BASE/1000);
mux.unlock();
XLOGI("total ms = %d!",totalMs);
GetVPara();
GetAPara();
return true;
}
//获取视频参数
XParameter FFDemux::GetVPara()
{
mux.lock();
if (!ic) {
mux.unlock();
XLOGE("GetVPara failed! ic is NULL!");
return XParameter();
}
//获取了视频流索引
int re = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, 0, 0);
if (re < 0) {
mux.unlock();
XLOGE("av_find_best_stream failed!");
return XParameter();
}
videoStream = re;
XParameter para;
para.para = ic->streams[re]->codecpar;
mux.unlock();
return para;
}
//获取音频参数
XParameter FFDemux::GetAPara()
{
mux.lock();
if (!ic) {
mux.unlock();
XLOGE("GetVPara failed! ic is NULL!");
return XParameter();
}
//获取了音频流索引
int re = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, 0, 0);
if (re < 0) {
mux.unlock();
XLOGE("av_find_best_stream failed!");
return XParameter();
}
audioStream = re;
XParameter para;
para.para = ic->streams[re]->codecpar;
para.channels = ic->streams[re]->codecpar->channels;
para.sample_rate = ic->streams[re]->codecpar->sample_rate;
mux.unlock();
return para;
}
//读取一帧数据,数据由调用者清理
XData FFDemux::Read()
{
mux.lock();
if(!ic)
{
mux.unlock();
return XData();
}
XData d;
AVPacket *pkt = av_packet_alloc();
int re = av_read_frame(ic,pkt);
if(re != 0)
{
mux.unlock();
av_packet_free(&pkt);
return XData();
}
//XLOGI("pack size is %d ptss %lld",pkt->size,pkt->pts);
d.data = (unsigned char*)pkt;
d.size = pkt->size;
if(pkt->stream_index == audioStream)
{
d.isAudio = true;
}
else if(pkt->stream_index == videoStream)
{
d.isAudio = false;
}
else
{
mux.unlock();
av_packet_free(&pkt);
return XData();
}
//转换pts
pkt->pts = pkt->pts * (1000*r2d(ic->streams[pkt->stream_index]->time_base));
pkt->dts = pkt->dts * (1000*r2d(ic->streams[pkt->stream_index]->time_base));
d.pts = (int)pkt->pts;
//XLOGE("demux pts %d",d.pts);
mux.unlock();
return d;
}
FFDemux::FFDemux()
{
static bool isFirst = true;
if(isFirst)
{
isFirst = false;
//注册所有封装器
av_register_all();
//注册所有的解码器
avcodec_register_all();
//初始化网络
avformat_network_init();
XLOGI("register ffmpeg!");
}
}
在cpp/IDecode.cpp中也是要處理贊同與休眠的處理:
在cpp/IDemux.cpp中也要處理贊同與休眠的問題:
在cpp/IPlayer.cpp中也要處理暫停、停止、綫程之間的邏輯:
Cpp/IPlayer.h中要有設置暫停的方法:
每次自然還是入口cpp/native-lib.cpp修改的最大吧:
Cpp/SLAudioPlay.cpp要增加判斷是否存在的邏輯:
Cpp/XThread.cpp中要處理暫停邏輯,等待100毫秒:
Cpp/XThread.h中也要聲明設置暫停的方法:
#include "XLog.h"
void IAudioPlay::Clear()
{
framesMutex.lock();
while(!frames.empty())
{
frames.front().Drop();
frames.pop_front();
}
framesMutex.unlock();
}
XData IAudioPlay::GetData()
{
XData d;
isRuning = true;
while(!isExit)
{
if(IsPause())
{
XSleep(2);
continue;
}
framesMutex.lock();
if(!frames.empty())
{
//有数据返回
d = frames.front();
frames.pop_front();
framesMutex.unlock();
pts = d.pts;
return d;
}
framesMutex.unlock();
XSleep(1);
}
isRuning = false;
//未获取数据
return d;
}
void IAudioPlay::Update(XData data)
{
//XLOGE("IAudioPlay::Update %d",data.pts);
//压入缓冲队列
if(data.size<=0|| !data.data) return;
while(!isExit)
{
framesMutex.lock();
if(frames.size() > maxFrame)
{
framesMutex.unlock();
XSleep(1);
continue;
}
frames.push_back(data);
framesMutex.unlock();
break;
}
}
#include "XLog.h"
//由主体notify的数据
void IDecode::Update(XData pkt)
{
if(pkt.isAudio != isAudio)
{
return;
}
while (!isExit)
{
packsMutex.lock();
//阻塞
if(packs.size() < maxList)
{
//生产者
packs.push_back(pkt);
packsMutex.unlock();
break;
}
packsMutex.unlock();
XSleep(1);
}
}
void IDecode::Clear()
{
packsMutex.lock();
while(!packs.empty())
{
packs.front().Drop();
packs.pop_front();
}
pts = 0;
synPts = 0;
packsMutex.unlock();
}
void IDecode::Main()
{
while(!isExit)
{
if(IsPause())
{
XSleep(2);
continue;
}
packsMutex.lock();
//判断音视频同步
if(!isAudio && synPts > 0)
{
if(synPts < pts)
{
packsMutex.unlock();
XSleep(1);
continue;
}
}
if(packs.empty())
{
packsMutex.unlock();
XSleep(1);
continue;
}
//取出packet 消费者
XData pack = packs.front();
packs.pop_front();
//发送数据到解码线程,一个数据包,可能解码多个结果
if(this->SendPacket(pack))
{
while(!isExit)
{
//获取解码数据
XData frame = RecvFrame();
if(!frame.data) break;
//XLOGE("RecvFrame %d",frame.size);
pts = frame.pts;
//发送数据给观察者
this->Notify(frame);
}
}
pack.Drop();
packsMutex.unlock();
}
}
#include "XLog.h"
void IDemux::Main()
{
while(!isExit)
{
if(IsPause())
{
XSleep(2);
continue;
}
XData d = Read();
if(d.size > 0)
Notify(d);
else
XSleep(2);
//XLOGI("IDemux Read %d",d.size);
//if(d.size<=0)break;
}
}
#include "IDemux.h"
#include "IDecode.h"
#include "IAudioPlay.h"
#include "IVideoView.h"
#include "IResample.h"
#include "XLog.h"
IPlayer *IPlayer::Get(unsigned char index)
{
static IPlayer p[256];
return &p[index];
}
void IPlayer::Main()
{
while (!isExit)
{
mux.lock();
if(!audioPlay|| !vdecode)
{
mux.unlock();
XSleep(2);
continue;
}
//同步
//获取音频的pts 告诉视频
int apts = audioPlay->pts;
//XLOGE("apts = %d",apts);
vdecode->synPts = apts;
mux.unlock();
XSleep(2);
}
}
void IPlayer::Close()
{
mux.lock();
//2 先关闭主体线程,再清理观察者
//同步线程
XThread::Stop();
//解封装
if(demux)
demux->Stop();
//解码
if(vdecode)
vdecode->Stop();
if(adecode)
adecode->Stop();
if(audioPlay)
audioPlay->Stop();
//2 清理缓冲队列
if(vdecode)
vdecode->Clear();
if(adecode)
adecode->Clear();
if(audioPlay)
audioPlay->Clear();
//3 清理资源
if(audioPlay)
audioPlay->Close();
if(videoView)
videoView->Close();
if(vdecode)
vdecode->Close();
if(adecode)
adecode->Close();
if(demux)
demux->Close();
mux.unlock();
}
double IPlayer::PlayPos()
{
double pos = 0.0;
mux.lock();
int total = 0;
if(demux)
total = demux->totalMs;
if(total>0)
{
if(vdecode)
{
pos = (double)vdecode->pts/(double)total;
}
}
mux.unlock();
return pos;
}
void IPlayer::SetPause(bool isP)
{
mux.lock();
XThread::SetPause(isP);
if(demux)
demux->SetPause(isP);
if(vdecode)
vdecode->SetPause(isP);
if(adecode)
adecode->SetPause(isP);
if(audioPlay)
audioPlay->SetPause(isP);
mux.unlock();
}
bool IPlayer::Seek(double pos)
{
bool re = false;
mux.lock();
//暂停所有线程
if(demux)
re = demux->Seek(pos);
mux.unlock();
return re;
}
bool IPlayer::Open(const char *path)
{
Close();
mux.lock();
//解封装
if(!demux || !demux->Open(path))
{
mux.unlock();
XLOGE("demux->Open %s failed!",path);
return false;
}
//解码 解码可能不需要,如果是解封之后就是原始数据
if(!vdecode || !vdecode->Open(demux->GetVPara(),isHardDecode))
{
XLOGE("vdecode->Open %s failed!",path);
//return false;
}
if(!adecode || !adecode->Open(demux->GetAPara()))
{
XLOGE("adecode->Open %s failed!",path);
//return false;
}
//重采样 有可能不需要,解码后或者解封后可能是直接能播放的数据
//if(outPara.sample_rate <= 0)
outPara = demux->GetAPara();
if(!resample || !resample->Open(demux->GetAPara(),outPara))
{
XLOGE("resample->Open %s failed!",path);
}
mux.unlock();
return true;
}
bool IPlayer::Start()
{
mux.lock();
if(vdecode)
vdecode->Start();
if(!demux || !demux->Start())
{
mux.unlock();
XLOGE("demux->Start failed!");
return false;
}
if(adecode)
adecode->Start();
if(audioPlay)
audioPlay->StartPlay(outPara);
XThread::Start();
mux.unlock();
return true;
}
void IPlayer::InitView(void *win)
{
if(videoView)
{
videoView->Close();
videoView->SetRender(win);
}
}
#define XPLAY_IPLAYER_H
#include <mutex>
#include "XThread.h"
#include "XParameter.h"
class IDemux;
class IAudioPlay;
class IVideoView;
class IResample;
class IDecode;
class IPlayer : public XThread
{
public:
static IPlayer *Get(unsigned char index=0);
virtual bool Open(const char *path);
virtual void Close();
virtual bool Start();
virtual void InitView(void *win);
//获取当前的播放进度 0.0 ~ 1.0
virtual double PlayPos();
virtual bool Seek(double pos);
virtual void SetPause(bool isP);
//是否视频硬解码
bool isHardDecode = true;
//音频输出参数配置
XParameter outPara;
IDemux *demux = 0;
IDecode *vdecode = 0;
IDecode *adecode = 0;
IResample *resample = 0;
IVideoView *videoView = 0;
IAudioPlay *audioPlay = 0;
protected:
//用作音视频同步
void Main();
std::mutex mux;
IPlayer(){};
};
#endif //XPLAY_IPLAYER_H
#include <string>
#include <android/native_window_jni.h>
#include "XLog.h"
#include "IPlayerPorxy.h"
extern "C"
JNIEXPORT
jint JNI_OnLoad(JavaVM *vm,void *res)
{
IPlayerPorxy::Get()->Init(vm);
/*IPlayerPorxy::Get()->Open("/sdcard/v1080.mp4");
IPlayerPorxy::Get()->Start();
IPlayerPorxy::Get()->Open("/sdcard/1080.mp4");
IPlayerPorxy::Get()->Start();*/
return JNI_VERSION_1_4;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_xplay_XPlay_InitView(JNIEnv *env, jobject instance, jobject surface) {
// TODO
ANativeWindow *win = ANativeWindow_fromSurface(env,surface);
IPlayerPorxy::Get()->InitView(win);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_xplay_OpenUrl_Open(JNIEnv *env, jobject instance, jstring url_) {
const char *url = env->GetStringUTFChars(url_, 0);
IPlayerPorxy::Get()->Open(url);
IPlayerPorxy::Get()->Start();
//IPlayerPorxy::Get()->Seek(0.5);
env->ReleaseStringUTFChars(url_, url);
}extern "C"
JNIEXPORT jdouble JNICALL
Java_com_example_xplay_MainActivity_PlayPos(JNIEnv *env, jobject instance) {
// TODO
return IPlayerPorxy::Get()->PlayPos();
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_xplay_MainActivity_Seek(JNIEnv *env, jobject instance, jdouble pos) {
IPlayerPorxy::Get()->Seek(pos);
}extern "C"
JNIEXPORT void JNICALL
Java_com_example_xplay_XPlay_PlayOrPause(JNIEnv *env, jobject instance) {
IPlayerPorxy::Get()->SetPause(!IPlayerPorxy::Get()->IsPause());
}
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
#include "XLog.h"
static SLObjectItf engineSL = NULL;
static SLEngineItf eng = NULL;
static SLObjectItf mix = NULL;
static SLObjectItf player = NULL;
static SLPlayItf iplayer = NULL;
static SLAndroidSimpleBufferQueueItf pcmQue = NULL;
SLAudioPlay::SLAudioPlay()
{
buf = new unsigned char[1024*1024];
}
SLAudioPlay::~SLAudioPlay()
{
delete buf;
buf = 0;
}
static SLEngineItf CreateSL()
{
SLresult re;
SLEngineItf en;
re = slCreateEngine(&engineSL,0,0,0,0,0);
if(re != SL_RESULT_SUCCESS) return NULL;
re = (*engineSL)->Realize(engineSL,SL_BOOLEAN_FALSE);
if(re != SL_RESULT_SUCCESS) return NULL;
re = (*engineSL)->GetInterface(engineSL,SL_IID_ENGINE,&en);
if(re != SL_RESULT_SUCCESS) return NULL;
return en;
}
void SLAudioPlay::PlayCall(void *bufq)
{
if(!bufq)return;
SLAndroidSimpleBufferQueueItf bf = (SLAndroidSimpleBufferQueueItf)bufq;
//XLOGE("SLAudioPlay::PlayCall");
//阻塞
XData d = GetData();
if(d.size<=0)
{
XLOGE("GetData() size is 0");
return;
}
if(!buf)
return;
memcpy(buf,d.data,d.size);
mux.lock();
if(pcmQue && (*pcmQue))
(*pcmQue)->Enqueue(pcmQue,buf,d.size);
mux.unlock();
d.Drop();
}
static void PcmCall(SLAndroidSimpleBufferQueueItf bf,void *contex)
{
SLAudioPlay *ap = (SLAudioPlay *)contex;
if(!ap)
{
XLOGE("PcmCall failed contex is null!");
return;
}
ap->PlayCall((void *)bf);
}
void SLAudioPlay::Close()
{
IAudioPlay::Clear();
mux.lock();
//停止播放
if(iplayer && (*iplayer))
{
(*iplayer)->SetPlayState(iplayer,SL_PLAYSTATE_STOPPED);
}
//清理播放队列
if(pcmQue && (*pcmQue))
{
(*pcmQue)->Clear(pcmQue);
}
//销毁player对象
if(player && (*player))
{
(*player)->Destroy(player);
}
//销毁混音器
if(mix && (*mix))
{
(*mix)->Destroy(mix);
}
//销毁播放引擎
if(engineSL && (*engineSL))
{
(*engineSL)->Destroy(engineSL);
}
engineSL = NULL;
eng = NULL;
mix = NULL;
player = NULL;
iplayer = NULL;
pcmQue = NULL;
mux.unlock();
}
bool SLAudioPlay::StartPlay(XParameter out)
{
Close();
mux.lock();
//1 创建引擎
eng = CreateSL();
if(eng)
{
XLOGI("CreateSL success! ");
}
else
{
mux.unlock();
XLOGE("CreateSL failed! ");
return false;
}
//2 创建混音器
SLresult re = 0;
re = (*eng)->CreateOutputMix(eng,&mix,0,0,0);
if(re !=SL_RESULT_SUCCESS )
{
mux.unlock();
XLOGE("SL_RESULT_SUCCESS failed!");
return false;
}
re = (*mix)->Realize(mix,SL_BOOLEAN_FALSE);
if(re !=SL_RESULT_SUCCESS )
{
mux.unlock();
XLOGE("(*mix)->Realize failed!");
return false;
}
SLDataLocator_OutputMix outmix = {SL_DATALOCATOR_OUTPUTMIX,mix};
SLDataSink audioSink= {&outmix,0};
//3 配置音频信息
//缓冲队列
SLDataLocator_AndroidSimpleBufferQueue que = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,10};
//音频格式
SLDataFormat_PCM pcm = {
SL_DATAFORMAT_PCM,
(SLuint32) out.channels,// 声道数
(SLuint32) out.sample_rate*1000,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_PCMSAMPLEFORMAT_FIXED_16,
SL_SPEAKER_FRONT_LEFT|SL_SPEAKER_FRONT_RIGHT,
SL_BYTEORDER_LITTLEENDIAN //字节序,小端
};
SLDataSource ds = {&que,&pcm};
//4 创建播放器
const SLInterfaceID ids[] = {SL_IID_BUFFERQUEUE};
const SLboolean req[] = {SL_BOOLEAN_TRUE};
re = (*eng)->CreateAudioPlayer(eng,&player,&ds,&audioSink,sizeof(ids)/sizeof(SLInterfaceID),ids,req);
if(re !=SL_RESULT_SUCCESS )
{
mux.unlock();
XLOGE("CreateAudioPlayer failed!");
return false;
} else{
XLOGI("CreateAudioPlayer success!");
}
(*player)->Realize(player,SL_BOOLEAN_FALSE);
//获取player接口
re = (*player)->GetInterface(player,SL_IID_PLAY,&iplayer);
if(re !=SL_RESULT_SUCCESS )
{
mux.unlock();
XLOGE("GetInterface SL_IID_PLAY failed!");
return false;
}
re = (*player)->GetInterface(player,SL_IID_BUFFERQUEUE,&pcmQue);
if(re !=SL_RESULT_SUCCESS )
{
mux.unlock();
XLOGE("GetInterface SL_IID_BUFFERQUEUE failed!");
return false;
}
//设置回调函数,播放队列空调用
(*pcmQue)->RegisterCallback(pcmQue,PcmCall,this);
//设置为播放状态
(*iplayer)->SetPlayState(iplayer,SL_PLAYSTATE_PLAYING);
//启动队列回调
(*pcmQue)->Enqueue(pcmQue,"",1);
isExit = false;
mux.unlock();
XLOGI("SLAudioPlay::StartPlay success!");
return true;
}
#include "XLog.h"
#include <thread>
using namespace std;
void XSleep(int mis)
{
chrono::milliseconds du(mis);
this_thread::sleep_for(du);
}
void XThread::SetPause(bool isP)
{
isPause = isP;
//等待100毫秒
for(int i = 0; i < 10; i++)
{
if(isPausing == isP)
{
break;
}
XSleep(10);
}
}
//启动线程
bool XThread::Start()
{
isExit = false;
isPause = false;
thread th(&XThread::ThreadMain,this);
th.detach();
return true;
}
void XThread::ThreadMain()
{
isRuning = true;
XLOGI("线程函数进入");
Main();
XLOGI("线程函数退出");
isRuning = false;
}
//通过控制isExit安全停止线程(不一定成功)
void XThread::Stop()
{XLOGI("Stop 停止线程begin!");
isExit = true;
for(int i = 0; i < 200; i++)
{
if(!isRuning)
{
XLOGI("Stop 停止线程成功!");
return;
}
XSleep(1);
}
XLOGI("Stop 停止线程超时!");
} #ifndef XPLAY_XTHREAD_H
#define XPLAY_XTHREAD_H
//sleep 毫秒
void XSleep(int mis);
//c++ 11 线程库
class XThread
{
public:
//启动线程
virtual bool Start();
//通过控制isExit安全停止线程(不一定成功)
virtual void Stop();
virtual void SetPause(bool isP);
virtual bool IsPause()
{
isPausing = isPause;
return isPause;
}
//入口主函数
virtual void Main() {}
protected:
bool isExit = false;
bool isRuning = false;
bool isPause = false;
bool isPausing = false;
private:
void ThreadMain();
};
#endif //XPLAY_XTHREAD_HOpenUrl.java其實變化也挺多:
package com.example.xplay;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
/**
* Created by Administrator on 2018-03-11.
*/
public class OpenUrl extends AppCompatActivity {
private Button btfile;
private Button btrtmp;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate( savedInstanceState );
setContentView( R.layout.openurl );
btfile = findViewById( R.id.playvideo );
btfile.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
EditText t = findViewById( R.id.fileurl );
//用户输入的URL,打开视频
Open(t.getText().toString());
//关闭当前窗口
finish();
}
}
);
btrtmp = findViewById( R.id.playrtmp );
btrtmp.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
EditText t = findViewById( R.id.rtmpurl );
//用户输入的URL,打开视频
Open(t.getText().toString());
//关闭当前窗口
finish();
}
}
);
}
public native void Open(String url);
}最後自然是XPlay.java了:
package com.example.xplay;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.View;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
/**
* Created by Administrator on 2018-03-04.
*/
public class XPlay extends GLSurfaceView implements SurfaceHolder.Callback,GLSurfaceView.Renderer, View.OnClickListener {
public XPlay(Context context, AttributeSet attrs) {
super( context, attrs );
//android 8.0 需要设置
setRenderer( this );
setOnClickListener( this );
}
@Override
public void surfaceCreated(SurfaceHolder holder)
{
//初始化opengl egl 显示
InitView(holder.getSurface());
//只有在绘制数据改变时才绘制view,可以防止GLSurfaceView帧重绘
//setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
@Override
public void surfaceChanged(SurfaceHolder var1, int var2, int var3, int var4)
{
}
@Override
public void surfaceDestroyed(SurfaceHolder var1)
{
}
public native void InitView(Object surface);
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
}
@Override
public void onSurfaceChanged(GL10 gl10, int i, int i1) {
}
@Override
public void onDrawFrame(GL10 gl10) {
}
@Override
public void onClick(View view) {
PlayOrPause();
}
public native void PlayOrPause();
}
关键字词:鎖死
相关文章
-
无相关信息