基于librtmp的安卓小项目:投屏摄像头视频:推流rtmp到服务器上并显示在其它设备上(比如电脑或者其它直播平台)
首先这个项目并未实现音频的传输,后面有时间再实现音频的传输后更新博文。这里如果是自己部署流媒体服务器,可以参考搭建nginx的相关博文,这里需要注意的是如果是搭建在linux系统下面,那么网络最好选用桥接模式,因为nat模式底下网络ip和手机不在同一个网段的,可以结合ffmpeg的ffplay(笔者为了项目整体性是用qt直接写好对应ffmpeg小型播放器)来测试播放效果。如果jni+cmake配置有问题的可以参考之前笔者博文,mediacodec有问题的话可以参考网上一些代码和笔者之前的踩坑日志,上面贴一个app封面的图,这里感谢雷神的部分演示代码和其它一些博文,不过事实上雷神的部分代码可读性不强(也可能我大四才疏学浅),其它某些博文也只是照搬东西,因此笔者对此进行一些改造后可以使用在该项目上。
要实现这项功能的大致流程是:在android studio那边要做好java层和c++层的连接,java层需要将推流的url地址先传给c++层进行rtmp_init,然后mediacodec层开始编码时,将视频数据、时间戳(pts)(这个其实也就是描述视频在什么时间点显示的相关参数,是递增的)、视频长度(此参数其实在c++层调用api来获取也可以)传给c++层,然后组包用RTMP_SEND送给rtmp服务器,然后在其它设备显示出来。
这里“送给rtmp服务器”的过程是有特点的:首先第一帧的数据可以获取sps/pps的信息,(这部分不推荐用雷神代码,太多while循环容易头晕)然后后面如果是关键帧,那么要先发送一下sps/pps的信息,然后再发送其余帧信息,如果不是关键帧直接把帧信息组包发出去就完事,这里就要注意你不能说在编码线程运行中间突然开启rtmp推流,这样就会错过sps/pps信息,服务器就不能正常收到信息,此时一般要先重置编码器然后再开启。
这里的“sps/pps”的长度以及帧信息是“不包含分割符的”,一般安卓手机摄像头每两帧之间都是用分割字符0x00,0x00,0x00,0x01来分开的,我们拿到的手机的帧信息(mediacodec解码后存储在bytebuffer中)要先把头部给拿掉,然后再做其它操作。
一般帧数据头部含分隔符的前5个数据通常为:0x00,0x00,0x00,0x01,0x67(sps)、0x00,0x00,0x00,0x01,0x68(pps)、0x00,0x00,0x00,0x01,0x41(普通帧)和0x00,0x00,0x00,0x01,0x65(关键帧),所以只要根据第5个数据就可以判断该帧数据里面放的数据信息属性。
然后时间戳的话,要先记录第一帧的bufferInfo.presentationTimeUs,然后用后面的presentationTimeUs扣掉第一帧的这个参数获取时间戳,据说一般手机摄像头的时间戳在30左右(笔者是33这样)。
这样代码的大致思路就清楚了,在写jni代码之前,首先为了方便打印日志,这边笔者用一个这个printLog函数来打印一些重要信息(%x一样可以打印十六进制数据,这里就不P出来了)。要注意这个方法打印数据时一定会回车打印,打印十六进制数据需要另外多输入几个%x。
#include <android/log.h>
void printLog(const char* _log)
{__android_log_print(ANDROID_LOG_INFO, "lclclc", "%s\n", _log); //log i类型
}void printLog(int _log)
{__android_log_print(ANDROID_LOG_INFO, "lclclc", "%d\n", _log); //log i类型
}
初始化和close RTMP函数如下:(这里用Live结构体存储sps pps数据)
typedef struct
{int16_t sps_len;int16_t pps_len;int8_t *sps;int8_t *pps;RTMP *rtmp;
}Live;RTMP* initRTMP(const char* url)
{RTMP* rtmp=RTMP_Alloc();RTMP_Init(rtmp);rtmp->Link.timeout=10;int ret=RTMP_SetupURL(rtmp,(char*)url);if(!ret){printLog("init URL fail");return NULL;}RTMP_EnableWrite(rtmp);ret=RTMP_Connect(rtmp,0);if(!ret){printLog("Connect fail");return NULL;}ret=RTMP_ConnectStream(rtmp,0);if(!ret){printLog("connect URL fail");return NULL;}live=(Live*)malloc(sizeof(Live));memset(live,0,sizeof(Live));live->rtmp=rtmp;return rtmp;
}void closeRTMP(RTMP* rtmp)
{if(rtmp!=NULL){RTMP_Close(rtmp);RTMP_Free(rtmp);free(live);}
}
然后我们要把sps.pps帧的信息提取出来,这里不用雷神的代码,P上我自己的代码
void preparevideo(int8_t* data,int len)
{int i=3;//首先是sps帧,我们要找到pps帧的头部位置while(i<len){if(i+4<len){if(data[i]==0x00&&data[i+1]==0x00&&data[i+2]==0x00&&data[i+3]==0x01&&data[i+4]==0x68){//这样sps的长度和大小就得到了live->sps_len=i-3;live->sps=(int8_t *) malloc(live->sps_len+1);memset(live->sps,0,sizeof(live->sps));memcpy(live->sps,data+4,live->sps_len);//后面计算pps长度和获取内容live->pps_len=len-i-4;live->pps=(int8_t *) malloc(live->pps_len + 1);memset(live->pps,0,sizeof(live->pps));memcpy(live->pps,&data[i+4],live->pps_len);break;}}++i;}
}
视频组包那边就直接用雷神的代码来搞了,组包格式如下:(从第一个字符开始)
1.关键帧:0x17;非关键帧:0x27
2.3.4.5.如果是sps/pps数据:0x00 0x00 0x00 0x00
如果是帧数据:0x01 0x00 0x00 0x00
6.7.8.9:如果是帧数据:待传输的数据长度的十六进制表示
如果是sps/pps数据那么比较复杂,后面另外陈述。
10以后:帧数据。
sps/pps的话:
版本号:0x01(占一个字节)、编码规格(sps[1].sps[2].sps[3]拼在一起占三个字节)(这里注意sps[1]不是0x67之类的,它一般是11的倍数)、几个字节的nalu包的长度、sps个数(占一个字节)、sps长度(占2字节)、sps内容、pps个数(1字节)、pps长度(2字节)、pps内容。
这里网上几乎都能copy到相关代码,内容大同小异,这里就不演示了。
此外这里为了接收Java层传输过来的jbyte数据,用了一个工具方法:
char* ConvertJByteaArrayToChars(JNIEnv *env, jbyteArray bytearray)
{char *chars = NULL;jbyte *bytes;bytes = env->GetByteArrayElements(bytearray, 0);int chars_len = env->GetArrayLength(bytearray);chars = new char[chars_len + 1];memset(chars,0,chars_len + 1);memcpy(chars, bytes, chars_len);chars[chars_len] = 0;env->ReleaseByteArrayElements(bytearray, bytes, 0);return chars;
}
接下来是发送数据的核心代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_mediaitem2_AvcEncoder_RTMP_1SEND(JNIEnv *env, jobject thiz, jbyteArray data,jint byte_len, jboolean is_key_frame,jlong time_stamp) {// TODO: implement RTMP_SEND()char* bytePtr=ConvertJByteaArrayToChars(env,data);printLog(byte_len);__android_log_print(ANDROID_LOG_INFO, "lclclc", "%x%x%x%x%x%x\n",bytePtr[0],bytePtr[1],bytePtr[2],bytePtr[3],bytePtr[4],bytePtr[5]); //log i类型if(bytePtr[4]==0x67){printLog("记录pps帧和sps帧数据");preparevideo(reinterpret_cast<int8_t *>(bytePtr), byte_len);char* body=(char*)live->sps;
// __android_log_print(ANDROID_LOG_INFO, "lclclc","%x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x\n",
// body[0],body[1],body[2],body[3],body[4],body[5],body[6],body[7],body[8],body[9],body[10],
// body[11],body[12],body[13],body[14],body[15],body[16],body[17]);body=(char*)live->pps;}else{if(bytePtr[4]==0x65){int ret=SendH264Packet(&bytePtr[4],byte_len-4,1,time_stamp);if(ret!=1){printLog("发送失败");}}else{int ret=SendH264Packet(&bytePtr[4],byte_len-4,0,time_stamp);if(ret!=1){printLog("发送失败");}}}}
java层encoded输出数据循环的处理,供参考:
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);while (outputBufferIndex >= 0) {if(isFirstFrame&&isRTMP){startTime=bufferInfo.presentationTimeUs/1000;isFirstFrame=false;}//Log.i("AvcEncoder", "Get H264 Buffer Success! flag = "+bufferInfo.flags+",pts = "+bufferInfo.presentationTimeUs+"");ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];byte[] outData = new byte[bufferInfo.size];outputBuffer.get(outData);
// System.out.println("转换成十六进制数据为:"+bytes2hex(outData));
// System.out.println("时间戳PTS为:"+(bufferInfo.presentationTimeUs/1000-startTime));if(bufferInfo.flags == BUFFER_FLAG_CODEC_CONFIG){configbyte = new byte[bufferInfo.size];configbyte = outData;if(isRTMP)RTMP_SEND(outData,bufferInfo.size,false,bufferInfo.presentationTimeUs/1000-startTime);}else if(bufferInfo.flags == BUFFER_FLAG_KEY_FRAME){System.out.println("这帧是关键帧");byte[] keyframe = new byte[bufferInfo.size + configbyte.length];System.arraycopy(configbyte, 0, keyframe, 0, configbyte.length);//把编码后的视频帧从编码器输出缓冲区中拷贝出来System.arraycopy(outData, 0, keyframe, configbyte.length, outData.length);if(isRTMP)RTMP_SEND(outData,outData.length,true,bufferInfo.presentationTimeUs/1000-startTime);if(isRecording)outputStream.write(keyframe, 0, keyframe.length);}else{//写到文件中if(isRTMP)RTMP_SEND(outData,bufferInfo.size,false,bufferInfo.presentationTimeUs/1000-startTime);if(isRecording)outputStream.write(outData, 0, outData.length);}mediaCodec.releaseOutputBuffer(outputBufferIndex, false);outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_USEC);
此处也copy上qt播放端用ffmpeg写的代码,除了路径以外可以直接拷贝使用(filePath要自己配置一下):(如果对配置ffmpeg+qt有问题的可以参考一下网上其它博文配置一下环境,觉得麻烦的也可以直接下载个ffmpeg用命令行,当然也听说有人用VLC播放器拉流,这个我也没用过)
头文件内容:
#ifndef WIDGET_H
#define WIDGET_H
#define _FIRST 1
#define _ZERO 0
#include <QWidget>
#include<QDebug>
#include<QPainter>
#include<QPaintEvent>
#include<QLabel>
#include<QCoreApplication>
#include<QTime>
#include<QTimer>
#include<QCameraInfo>
#include<iostream>
#include<QSlider>
extern "C"{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavdevice/avdevice.h>
#include <libavformat/version.h>
#include <libavutil/time.h>
#include <libavutil/mathematics.h>
#include <libavutil/imgutils.h>
}
using namespace std;
class Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = 0);QLabel *label,*timeLabel;QTimer timer,timer2;QSlider *slider;bool flag;uint8_t* buffer;AVFrame *pFrameRGB;//保存文件容器封装信息和码流参数的结构体AVFormatContext *pFormatCtx=NULL;//解码器上下文对象,解码器依赖的相关环境,状态,资源以及参数集接口指针AVCodecContext *pCodecCtx=NULL;AVPacket *packet;//提供编码器和解码器的公共接口AVCodec *pCodec=NULL;//保存音视频解码后的数据,包括状态信息,编码解码信息,QP表,宏块类型表,运动矢量表灯AVFrame *pFrame=NULL;//描述转换器参数的结构体struct SwsContext *sws_ctx=NULL;AVDictionary *optionsDict=NULL;//循环变量,视频流类型编号int i,videoStream;//解码操作成功标识int frameFinished;double frameTime=0;int nextValue=0,befoValue=0;int curmin=0,curs=0,curhour=0;int frameDuration=0;~Widget();void Delay(int msec);void on_btnPlay_clicke();double readFrame();// void prepareRead();
// void paintEvent(QPaintEvent *event);void paint();public slots:void setFrame(QPixmap &pixmap);
};#endif // WIDGET_H
cpp代码:
#include "widget.h"Widget::Widget(QWidget *parent): QWidget(parent)
{label=new QLabel(this);label->setGeometry(0,0,this->width(),this->height()-50);label->show();slider=new QSlider(Qt::Horizontal,this);slider->setValue(0);slider->setGeometry(0,this->height()-40,this->width(),20);slider->setMinimum(0);slider->show();timeLabel=new QLabel(this);timeLabel->setGeometry(this->width()-50,this->height()-20,50,20);timeLabel->setText("0:0:0");// connect(&timer2,&QTimer::timeout,[=](){
// flag=false;
// timer2.stop();
// });connect(slider,&QSlider::valueChanged,[=](){timer.stop();//当前进度条的读数int currValue=slider->value();//转换成当前时间=总时间*当前读数/总数int curTimes=currValue/1000000;//计算pts
// int pts=curTimes;qDebug()<<"pts="<<curTimes/av_q2d(pFormatCtx->streams[videoStream]->time_base);//跳转帧int ret=av_seek_frame(pFormatCtx,videoStream,/*1000000*curTimes/19*/curTimes/av_q2d(pFormatCtx->streams[videoStream]->time_base),AVSEEK_FLAG_ANY);qDebug()<<curTimes;//1000000 42-24=18if(ret<0){qDebug()<<"fail";return;}//底下的时间也要跟着改//秒int s=curTimes%60;int min=curTimes-s;int hour=0;if(min>=3600){int i=0;while(min-3600*i>=60&&++i);min=(min-3600*i)/60;hour=i;}else{min=min/60;}curs=s;curmin=min;curhour=hour;QString timeStr;timeStr+=QString::number(curhour);timeStr+=":";timeStr+=QString::number(curmin);timeStr+=":";timeStr+=QString::number(curs);timeLabel->setText(timeStr);befoValue=curTimes;// readFrame();timer.start();});on_btnPlay_clicke();// prepareRead();}Widget::~Widget()
{}void Widget::Delay(int msec)
{QTime dieTime = QTime::currentTime().addMSecs(msec);while( QTime::currentTime() < dieTime )QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}void Widget::on_btnPlay_clicke()
{QString cameraName;//获取摄像头的信息QList<QCameraInfo> cameras=QCameraInfo::availableCameras();foreach(const QCameraInfo &cameraInfo,cameras){//get camera NamecameraName=cameraInfo.description();qDebug()<<cameraName;}//注册所有ffmpeg支持的多媒体格式及编解码器av_register_all();AVInputFormat *inputFormat = av_find_input_format("dshow");string pathName="video=";pathName+=cameraName.toStdString();AVDictionary* options=NULL;av_dict_set(&options,"buffer_size","1024000",0);av_dict_set(&options,"max_delay","500000",0);av_dict_set(&options,"stimeout","20000000",0);av_dict_set(&options,"rtsp_transport", "tcp", 0);pathName="rtmp://192.168.0.103:1935/live";
// pathName="E://video.wmv";
// pathName="rtsp://192.168.78.128:8554/test.264";
// pathName="rtmp://192.168.78.128:8554/livestream";// pathName="rtmp://sendtc3a.douyu.com/live/11387593rZqiBvMZ?wsSecret=b5f70a2285186942e1e4c2be84b78ae4&wsTime=63c4e15d&wsSeek=off&wm=0&tw=0&roirecognition=0&record=flv&origin=tct&txHost=sendtc3a.douyu.com";
// pathName="E://test1.avi";//打开视频文件,读文件头内容,取得文件容器的封装信息及码流参数并存储在pFormatCtx中if(avformat_open_input(&pFormatCtx,(const char*)pathName.data(),NULL,&options)!=0){qDebug()<<"open file fail";return ;}//尝试获取文件中保存的码流信息,并填充到pFormatCtx->stream字段中if(avformat_find_stream_info(pFormatCtx,NULL)<0){qDebug()<<"get code stream fail";return;}videoStream=-1;//确认文件的所有流媒体类型中包含视频流类型for(i=0;i<pFormatCtx->nb_streams;++i){if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){videoStream=i;break;}}//如果没有找到视频流就退出if(videoStream==-1){qDebug()<<"no video stream";return;}
// videoStream=av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);//根据流类型标号从pFormatCtx->streams中取得视频流对应的解码器上下文pCodecCtx=pFormatCtx->streams[videoStream]->codec;//根据视频流对应的解码器上下文查找对应的解码器,返回对应的解码器(信息结构体)pCodec=avcodec_find_decoder(pCodecCtx->codec_id);//如果找不到符合条件的编码器if(pCodec==NULL){qDebug()<<"can't find decoder";return;}pFrame = av_frame_alloc();//存放从AVPacket中解码出来的原始数据pFrameRGB = av_frame_alloc();//存放原始数据转换的目标数据packet = av_packet_alloc();av_new_packet(packet, pCodecCtx->width * pCodecCtx->height);//分配packet的有效载荷并初始化其字段//打开编码器if(avcodec_open2(pCodecCtx,pCodec,&optionsDict)<0){qDebug()<<"decoder open fail";return;}// Allocate video frame,为解码后的视频信息结构体分配空间并完成初始化操作(结构体中的图像缓存按照下面两步手动安装)//设置图像的转换格式sws_ctx=sws_getContext(pCodecCtx->width,pCodecCtx->height,//转换图片之前的宽高、格式和之后的高宽和格式pCodecCtx->pix_fmt,pCodecCtx->width,pCodecCtx->height,AV_PIX_FMT_RGB32,SWS_BICUBIC,NULL,NULL,NULL);int numBytes = avpicture_get_size(AV_PIX_FMT_RGB32, pCodecCtx->width,pCodecCtx->height);//(弃用函数)
// qDebug() << numBytes;// buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
// /*瓜分分配的空间*/
// //瓜分上一步分配到的buffer.// if(av_image_fill_arrays(pFrameRGB->data, // 需要填充的图像数据指针
// pFrameRGB->linesize,
// buffer,
// AV_PIX_FMT_RGB32, //图像的格式
// pCodecCtx->width,
// pCodecCtx->height,
// 1) < 0) //图像数据中linesize的对齐
// {
// qDebug()<<"获取流图像失败";
// return;
// }auto buf = static_cast<uchar *>(av_malloc(static_cast<size_t>(av_image_get_buffer_size(AV_PIX_FMT_RGB32,pCodecCtx->width,pCodecCtx->height, 1))));// qDebug()<<"时长为:"<<pFormatCtx->duration/1000000.0;qDebug()<<pFormatCtx->duration;slider->setMaximum(pFormatCtx->duration);qDebug()<<"set value succ";//根据后5个参数的内容填充前两个参数,成功返回源图像的大小,失败返回一个负值if(av_image_fill_arrays(pFrameRGB->data, // 需要填充的图像数据指针pFrameRGB->linesize,buf,AV_PIX_FMT_RGB32, //图像的格式pCodecCtx->width,pCodecCtx->height,1) < 0) //图像数据中linesize的对齐{qDebug()<<"获取流图像失败";return;}// int y_size = pCodecCtx->width * pCodecCtx->height;
// packet = (AVPacket *) malloc(sizeof(AVPacket)); //申请一个视频帧包的大小
// av_new_packet(&packet, y_size); //分配packet的数据,为packet分配一个指定大小的内存
// avpicture_fill((AVPicture *)pFrameRGB, buffer, AV_PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height);QObject::connect(&timer,QTimer::timeout,[=](){readFrame();});qDebug()<<pFormatCtx->streams[videoStream]->nb_frames<<pFormatCtx->duration;frameTime=1000/pFormatCtx->streams[videoStream]->avg_frame_rate.num;
// if(pFormatCtx->streams[videoStream]->nb_frames!=0)
// {
// frameTime=pFormatCtx->duration/pFormatCtx->streams[videoStream]->nb_frames/1000;
// }
// else
// {
// frameTime=pFormatCtx->duration/420/1000;
// qDebug()<<pFormatCtx->streams[videoStream]->avg_frame_rate.num;
// }timer.start();//每帧时间(单位:ms)qDebug()<<"==========================================================================================";}double Widget::readFrame()
{
// flag=true;// timer2.start(frameTime);if(av_read_frame(pFormatCtx,packet)>=0){
// qDebug()<<")))))))))))))";if(packet->stream_index==videoStream){/*-----------------------* Decode video frame,解码完整的一帧数据,并将frameFinished设置为true* 可能无法通过只解码一个packet就获得一个完整的视频帧frame,可能需要读取多个packet才行* avcodec_decode_video2()会在解码到完整的一帧时设置frameFinished为真* Technically a packet can contain partial frames or other bits of data* ffmpeg's parser ensures that the packets we get contain either complete or multiple frames* convert the packet to a frame for us and set frameFinisned for us when we have the next frame-----------------------*///=========================解码方法一==========================================//
// int ret = avcodec_send_packet(pCodecCtx, packet); //发送数据到ffmepg,放到解码队列中
// if (ret < 0)
// {
// qDebug()<<"decode failed";
// return;
// }
// int got_picture = avcodec_receive_frame(pCodecCtx, pFrame); //将成功的解码队列中取出1个frame
// if(got_picture)
// {
// qDebug()<<"get failed";
// return;
// }int ret=avcodec_decode_video2(pCodecCtx,pFrame,&frameFinished,packet);sws_scale(sws_ctx,static_cast<const uchar* const*>(pFrame->data),pFrame->linesize,0,pCodecCtx->height,pFrameRGB->data,pFrameRGB->linesize);// qDebug()<<QPixmap pixmap = QPixmap::fromImage(QImage(static_cast<uchar*>(pFrameRGB->data[0]),pCodecCtx->width,pCodecCtx->height,QImage::Format_RGB32).scaled(label->width(),label->height()));
// label->setPixmap(QPixmap::fromImage(QImage((uchar*)buffer,
// pFrameRGB->width,
// pFrameRGB->height,QImage::Format_RGB32)));
// frameDuration=packet->duration;// qDebug()<<packet->dts;nextValue=av_q2d(pFormatCtx->streams[videoStream]->time_base)*packet->pts;// qDebug()<<packet->fps;if(nextValue>=befoValue+1){befoValue=nextValue;slider->blockSignals(true);slider->setValue(av_q2d(pFormatCtx->streams[videoStream]->time_base)*packet->pts*1000000);slider->blockSignals(false);++curs;if(curs==60){curs=0;++curmin;if(curmin==60){curmin=0;++curhour;}}QString timeStr;timeStr+=QString::number(curhour);timeStr+=":";timeStr+=QString::number(curmin);timeStr+=":";timeStr+=QString::number(curs);timeLabel->setText(timeStr);}label->setPixmap(pixmap);av_packet_unref(packet);Delay(frameTime);
// Delay(40);
// }}}else{qDebug()<<"movie end";
// av_free(pFrame);
// avcodec_close(pCodecCtx);
// avformat_close_input(&pFormatCtx);timer.stop();}}void Widget::paint()
{}void Widget::setFrame(QPixmap &pixmap)
{}
// https://blog.csdn.net/m0_48578207/article/details/107372494
基于librtmp的安卓小项目:投屏摄像头视频:推流rtmp到服务器上并显示在其它设备上(比如电脑或者其它直播平台)相关推荐
- 安卓手机如何投屏到电视上_手机如何投屏到电视上?小屏秒变大屏,追剧更享受!...
作为一名上班族,如果你平时休息的时间非常喜欢宅在家里追剧的话,那么估计你在追剧时,都会使用手机,而放弃里家里的电视.但是你知道吗,其实利用家里的电视来追手机上的电视剧,要比你用手机直接追剧来的更爽快, ...
- 基于DLNA实现iOS、Android投屏:基本概念
http://geek.csdn.net/news/detail/58920 由于我司需求,需要在iOS和安卓客户端实现DLNA投屏和控制.经过一番折腾,决定由我来研究DLNA.说起来又兴奋又紧张,兴 ...
- 基于DLNA实现iOS、Android投屏
由于我司需求,需要在iOS和安卓客户端实现DLNA投屏和控制.经过一番折腾,决定由我来研究DLNA.说起来又兴奋又紧张,兴奋希望自己能够弄出来然后跟安卓组讲解原理,紧张是因为怕自己能力不足做不出来. ...
- tc溜溜865手机投屏卡_溜溜tcgames老版本(电脑玩手机游戏)-溜溜TC Games32位/64位旧版本PC下载V2.0.0官网安卓真机投屏-西西软件下载...
溜溜TC Games32位/64位旧版本PC是一款非常好用的手机游戏投屏工具,有了这款软件我们就可以将手机上的游戏画面投入到电脑上,大屏幕玩游戏,这样肯定会跟畅快,该软件是由成都杰华科技有限公司基于P ...
- android投影到创维电视,创维电视怎么投屏?图文讲解安卓和苹果手机投屏到创维电视方法...
创维电视应该怎么投屏呢?想把手机里看的视频投屏到电视?却不知道怎么投屏?今天蜜罐蚁小编给大家介绍下安卓手机和苹果手机投屏到创维电视的方法供大家参考. 目前电视投屏有三大方法,电视自带投屏软件.手机视频 ...
- android电视投影ipad,【沙发管家】苹果手机, Ipad连接安卓智能电视投屏教程!
原标题:[沙发管家]苹果手机, Ipad连接安卓智能电视投屏教程! 现在很多人对于安卓智能电视的投屏功能已经非常熟悉,使用安卓手机的用户,有很多办法可以直接连接电视进行投屏,但是,使用苹果设备直连安卓 ...
- 基于DLNA实现iOS,Android投屏:SSDP发现设备
SSDP能够在局域网能简单地发现设备提供的服务.SSDP有两种发现方式:主动通知和搜索响应方式. 寻址 UPnP 技术是架构在 IP 网络之上.因此拥有一个网络中唯一的 IP 地址是 UPnP 设备正 ...
- android电视投影ipad,【沙发管家】苹果手机,,iPad连接安卓智能电视投屏方法
原标题:[沙发管家]苹果手机,,iPad连接安卓智能电视投屏方法 现在很多人对于安卓智能电视的投屏功能已经非常熟悉,使用安卓手机的用户,有很多办法可以直接连接电视进行投屏,但是,使用苹果设备直连安卓智 ...
- 安卓手机如何投屏到电视上_创维电视怎么投屏?图文讲解安卓和苹果手机投屏到电视方法...
创维电视应该怎么投屏呢?想把手机里看的视频投屏到电视?却不知道怎么投屏?今天蜜罐蚁小编给大家介绍下安卓手机和苹果手机投屏到创维电视的方法供大家参考. 目前电视投屏有三大方法,电视自带投屏软件.手机视频 ...
最新文章
- 42HS48步进电机实验
- 最快的PNG图像解码器!速度提升2.75倍,比老大哥“libpng”还安全
- python第三方库文件传输助手_python实现文件助手中查看微信撤回消息
- 线性代数 第四章 向量组的线性相关性
- window oracle 只有bak文件怎么恢复_Oracle 11g R2 RAC数据库备份通过RMAN恢复到单实例数据库实现...
- mysql主从复制缺陷_mysql主从复制及遇到的坑
- 问题与事务跟踪系统jira中的版本管理
- Springboot整合RocketMQ实战
- 【luogu3834】【POJ2104】【模板】可持久化线段树 1 [主席树]
- Jersey +jetty 实现微服务(一)
- 认识JWT(JSON WEB TOKEN)
- word文档中页眉页脚的设置问题
- nodejs redis 发布订阅_「赵强老师」Redis的消息发布与订阅
- 路径的形式不合法解决方案
- springMVC学习2
- 嵌入式开发(二):开发板配置(自用)
- 复制粘贴,快速将Python程序打包成exe
- 使用MAC中碰到的各种问题
- 魔兽世界诞生记(下)
- Word操作之Mathtype自动进行公式编号
热门文章
- python求和:1/3+3/5+5/7+7/9+...+97/99
- 2021程序员必看面试指南-进大厂年薪百万需要付出多少努力?你看看你们配吗......
- Qt自定义控件创建和使用
- noi 2017 简要题解
- 如何选择自己心仪的U盘
- python显示透明图片背景
- uni-app 小程序 微信订阅消息通知
- QGIS基于多期哨兵2影像遥感指数阈值法提取冬小麦分布(1)-数据预处理
- D5渲染器电脑硬件配置Vol.1——操作系统丨显卡
- 读《楚汉传奇》中历史故事悟项目管理