Android端录制视频,.NET实时播放
先列关键词:Android录制、SOCKET转发、.NET实时播放,有兴趣的请往下看。
这是一个大工程,兜兜转转,花了不少时间,千真万确,玩成功了。
这是一个小工程,和QQ视频聊天比起来还很low,真的只能玩玩。
不管是大工程还是小工程,相信我,到2015年1月1日为止,这是网上能给搜到的唯一一篇这么玩,且实现播放了的文章。
什么,你跟我提YUV?好吧,我承认这是能实现的。。。就是只怕4G网络环境都玩不转
闲话不提,先说具体思路:.NET客户端发出连接指令→服务器转发→Android端接收→手机端录制→服务器转发→.NET客户端播放。噢。。这还是闲话,而且有点复杂,进入主题。
按上面的流程,首先,网页端发送与Android进行视频的指令,这个其实就是个socket通信,发一条指令,这个指令包括视频命令的标识和相应的ANDROID客户端标识。所谓视频命令标识,是指服务器端能给识别出,这条信号是要转发给ANDROID端的,Android端要能识别出,这条信号,是为了要来录制视频传上去的。至于.NET客户端是什么玩意,怎么连接SOCKET服务器,就写了,ASP有ASP的搞法,WINFORM有WINFORM的搞法,SL有SL的搞法,在此就不表了,提醒一下,SL好像是要有一个安全验证的,如果我没记错。
然后是服务器转发,这个服务器我是采用windows service 写的,里面开两个socket服务,一个端口用于接收和发送指令之类的信号,一个端口专门转发视频信号。如果单功能的话其实可以只用一个端口的,我做的时候还有其他功能,为了避免传视频的时候其他阻塞其他功能的信号,就开了两个端口。C#中开SOCKET端口的方法:
private Socket GetSocketServer(int serverPort)
{Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);socket.Bind(new IPEndPoint(IPAddress.Any, serverPort));socket.Listen(40);return socket;
}
转发视频指令代码:
else if (ByteCompare(flag, MessageFlag.SVdCommand))
{try{Log.Write(0, "Client", this.ClientName + "发送视频指令");Client phoneClient = null;string phoneName = Encoding.UTF8.GetString(info);//找出相应的手机端for (int i = 0; i < this.Server.ClientList.Count; i++){if (this.Server.ClientList[i].ClientName == phoneName){phoneClient = this.Server.ClientList[i];break;}}if (phoneClient != null){phoneClient.CurrentSocket.Send(flag);Socket webMediaSocket = this.Server.MediaSocket.Accept();Log.Write(0, "Client", this.ClientName + "准备好接收视频信号");MediaClient webMediaClient = new MediaClient(this.Server, webMediaSocket);webMediaClient.ClientName = this.ClientName;//接收手机端的连接Socket phoneSocket = this.Server.MediaSocket.Accept();Log.Write(0, "Client", phoneClient.ClientName + "准备发送收视频信号");MediaClient phoneMediaClient = new MediaClient(this.Server, phoneSocket);phoneMediaClient.ClientName = phoneClient.ClientName;this.Server.MediaClientList.Add(phoneMediaClient);webMediaClient.OpClient = phoneMediaClient;phoneMediaClient.OpClient = webMediaClient;Thread t = new Thread(phoneMediaClient.ReceiveClient);Log.Write(0, "Client", phoneClient.ClientName + "开始发送收视频信号");t.Start();}else{Log.Write(1, "Client", this.ClientName + "发送视频指令时未找到对应的手机");}}catch (Exception ex){Log.Write(1, "Client", "请求视频指令时发生错误,请查看错误日志!");Log.WriteError(ex.ToString());}
}
ByteCompare是自定义的Byte数组比较的方法,其中的flag就是上文说过的指令,后面的则是预制的指令枚举。ClientList是连接到服务器的所有手机客户端,从中找出指定的客户端,发送信号。与此同时,.NET客户端连接上视频服务器,等待手机端连接上视频服务器。当两者都连上之后,给两者配对,在之后的岁月里,配对的客户端将通过客户端,手牵着手,同0共1。
接下来就是手机端接收了。接收也没什么好说的,就是SOCKET接收,接收完了就比较:
else if (CompareByte(flag, InfoFlags.SVdCommand))
{//接收视频指令if (videoListener != null) {Object source = new Object();videoListener.onVideoConnect(new BaseEvent(source));}
}
上面定义了一个事件监听,监听视频的连接和断开,监听内容:
SocketClient.setVideoListener(new VideoListener() {@Overridepublic void onVideoDisconnect(BaseEvent e) { if (GlobalActivity.RecorderInstance != null) {GlobalActivity.RecorderInstance.StopRecord(null);}}@Overridepublic void onVideoConnect(BaseEvent e) {NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);int icon = R.drawable.message;Intent intent = new Intent(MainActivity.this,RecorderActivity.class);PendingIntent pendingIntent = PendingIntent.getActivity(MainActivity.this, 0, intent, 0);Notification.Builder builder = new Notification.Builder(MainActivity.this);builder.setDefaults(Notification.DEFAULT_ALL).setTicker("接收到来自网页端的视频命令!").setContentTitle("视频命令").setContentText("接收到来自网页端的视频命令!接收请点击信息,拒绝请清除信息!").setSmallIcon(icon).setContentIntent(pendingIntent);Notification notification = builder.build();notification.flags = Notification.FLAG_AUTO_CANCEL;notificationManager.notify(0, notification);}
});
其实就是接收到之后,就给手机发一通知,点那个通知,就进入拍摄界面。
自然而然,就开始录制了,录制界面吧,直接看代码,结构简单至极,不占篇幅:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#0099cc"tools:context="com.ljnewmap.rlxj.RecorderActivity" ><SurfaceViewandroid:id="@+id/record_preview_surfaceView"android:layout_width="fill_parent"android:layout_height="fill_parent"android:layout_gravity="center" /><ImageButtonandroid:id="@+id/startButton"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginBottom="8dp"android:layout_alignParentRight= "true"android:layout_centerVertical="true" android:background="@layout/img_stop_press"android:contentDescription="@string/app_name"android:onClick="StopRecord" /></RelativeLayout>
不得不说,还是相对布局比较好用,轻松自在就把按钮放在下面的中间了。。用线性布局简直复杂如狗。。。。。。
接下来自然而然就是录制的后台和上传了,录制采用H264编码,经过测试,480P的视频需要的网速是128K,就是1M网,基本上是3G网的上限,稍微降一些分辨率,降一些码率,应该是能符合现实需求的。上传当然采用SOCKET上传。坑爹的是,直接录制的H264码传上去是不能播放的,所以需要稍作处理。代码我会直接贴在下面,具体原理请见大神zblue78的博客:http://blog.csdn.net/zblue78/article/details/6078040 和http://blog.csdn.net/zblue78/article/details/6083374 我和大神不同的地方仅仅在于,他是直接把byte写成了文件,我则是将其通过socket发了出去。代码如下:
package com.ljnewmap.rlxj;import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;import com.ljnewmap.rlxj.R;
import com.ljnewmap.rlxj.dal.PPSandSPS;
import com.ljnewmap.rlxj.global.ExitApplication;
import com.ljnewmap.rlxj.global.Global;
import com.ljnewmap.rlxj.global.GlobalActivity;import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.graphics.PixelFormat;
import android.hardware.Camera;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.Bundle;
import android.os.Environment;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;/*** An example full-screen activity that shows and hides the system UI (i.e.* status bar and navigation/system bar) with user interaction.* * @see SystemUiHider*/
public class RecorderActivity extends Activity implements SurfaceHolder.Callback,
MediaRecorder.OnErrorListener, MediaRecorder.OnInfoListener{private Socket ClientSocket;private MediaRecorder mMediaRecorder = null;private Camera camera;private boolean mMediaRecorderRecording = false; private SurfaceView mSurfaceView = null; private SurfaceHolder mSurfaceHolder = null; private LocalSocket receiver, sender; private LocalServerSocket lss; private Thread t; private boolean run = false;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 选择支持半透明模式,在有surfaceview的activity中使用。getWindow().setFormat(PixelFormat.TRANSLUCENT);// 去掉标题requestWindowFeature(Window.FEATURE_NO_TITLE); // 全屏getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);setContentView(R.layout.activity_recorder);mSurfaceView = (SurfaceView) this.findViewById(R.id.record_preview_surfaceView);mSurfaceHolder= mSurfaceView.getHolder(); mSurfaceHolder.addCallback(this);ExitApplication.getInstance().addActivity(this);GlobalActivity.RecorderInstance = this;}private void ConectLocalSocket(){if (receiver == null) {receiver = new LocalSocket();}try { lss = new LocalServerSocket("Local_Socket"); LocalSocketAddress localSocketAddress = new LocalSocketAddress("Local_Socket");receiver.connect(localSocketAddress); receiver.setReceiveBufferSize(500000); receiver.setSendBufferSize(500000); sender = lss.accept();sender.setReceiveBufferSize(500000);sender.setSendBufferSize(500000);} catch (IOException e) { return; }}private void startVideoRecording() { t = new Thread() { @Overridepublic void run() { try {while(ClientSocket == null){ClientSocket = new Socket(Global.Host, Global.Media_port);Thread.sleep(200);}} catch (Exception e) {}int frame_size = 1024;if(sender == null || receiver == null || lss == null){ConectLocalSocket();}byte[] buffer = new byte[1024*64];InputStream fis = null;//DataOutputStream outputStream = null;OutputStream outputStream = null;try {fis = receiver.getInputStream();outputStream = ClientSocket.getOutputStream();} catch (Exception e) {}initializeVideo();mMediaRecorder.start(); DataInputStream dis=new DataInputStream(fis); try { dis.read(buffer,0,44); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); }String sampleFilePath = Environment.getExternalStorageDirectory().getPath() +"/RLXJ/sample.h264";File samplefile = new File(sampleFilePath);if (!samplefile.exists()) {return;}byte[] h264sps = null; byte[] h264pps = null;try {PPSandSPS PpSandSPS = new PPSandSPS(sampleFilePath);h264sps = PpSandSPS.SPS;h264pps = PpSandSPS.PPS;} catch (IOException e2) {// TODO Auto-generated catch blockreturn;}byte[] h264head={0,0,0,1}; try { outputStream.write(h264head,0,h264head.length);outputStream.write(h264sps,0,h264sps.length); outputStream.write(h264head,0,h264head.length); outputStream.write(h264pps,0,h264pps.length); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } while (true) { try { //读取每场的长度 int h264length=dis.readInt();if (h264length > 50000 || h264length < 0) {while(true){byte byte1,byte2,byte3,byte4;byte1 = dis.readByte();if(byte1 == 0){byte2 = dis.readByte();if(byte2 == 0){byte3 = dis.readByte();byte4 = dis.readByte();int value; value = (int) ( ((byte1 & 0xFF)<<24) |((byte2 & 0xFF)<<16) |((byte3 & 0xFF)<<8) |(byte4 & 0xFF)); if (value > 0 && value < 50000) {h264length = value;break;}}}}}int number =0; outputStream.write(h264head,0,h264head.length); while(number<h264length) { int lost=h264length-number; int num = fis.read(buffer,0,frame_size<lost?frame_size:lost); number+=num; outputStream.write(buffer, 0, num); } } catch (Exception e) { break; } } try {ClientSocket.shutdownInput();ClientSocket.shutdownOutput();ClientSocket.close();} catch (Exception e) {// TODO: handle exception}}};t.start();} private boolean initializeVideo() {try {if (mSurfaceHolder == null) return false; mMediaRecorderRecording = true; if (mMediaRecorder == null) mMediaRecorder = new MediaRecorder(); else mMediaRecorder.reset(); if(camera == null)camera = Camera.open(0);camera.unlock();mMediaRecorder.setCamera(camera);mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P);if(profile != null){profile.videoFrameRate = 20;profile.videoBitRate = 1000000;profile.fileFormat = MediaRecorder.OutputFormat.MPEG_4;profile.videoFrameRate = 24;profile.videoCodec = MediaRecorder.VideoEncoder.H264;profile.audioCodec = MediaRecorder.AudioEncoder.AAC;mMediaRecorder.setProfile(profile);}// 预览mMediaRecorder.setPreviewDisplay(mSurfaceHolder.getSurface()); //设置以流方式输出mMediaRecorder.setOutputFile(sender.getFileDescriptor()); //mMediaRecorder.setOutputFile( Environment.getExternalStorageDirectory().getPath() + "/Video/45.mp4"); try { mMediaRecorder.setOnInfoListener(this); mMediaRecorder.setOnErrorListener(this); mMediaRecorder.prepare();} catch (Exception exception) { releaseMediaRecorder(); finish(); return false; } return true; } catch (Exception e) {releaseMediaRecorder(); return false;}}private void releaseMediaRecorder() { if (mMediaRecorder != null) { if (mMediaRecorderRecording) { try { mMediaRecorder.setOnErrorListener(null); mMediaRecorder.setOnInfoListener(null); mMediaRecorder.stop(); } catch (RuntimeException e) { System.out.println("stop fail: " + e.getMessage()); } mMediaRecorderRecording = false; } mMediaRecorder.reset(); mMediaRecorder.release(); camera.lock();camera.release();mMediaRecorder = null; } }@Overridepublic void surfaceCreated(SurfaceHolder holder) { // 将holder,这个holder为开始在oncreat里面取得的holder,将它赋给surfaceHolder mSurfaceHolder = holder;//initializeVideo();} @Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // 将holder,这个holder为开始在oncreat里面取得的holder,将它赋给surfaceHolder mSurfaceHolder = holder;if(camera == null){camera = Camera.open(0);}} @Overridepublic void surfaceDestroyed(SurfaceHolder holder) { // surfaceDestroyed的时候同时对象设置为null mSurfaceView = null; mSurfaceHolder = null; mMediaRecorder = null; camera.release();} @Override public void onInfo(MediaRecorder mr, int what, int extra) { } @Override public void onError(MediaRecorder mr, int what, int extra) { } public void StopRecord(View view) {if(!run){startVideoRecording();run = true;}else { if(t != null){t.interrupt();}releaseMediaRecorder();try {lss.close();receiver.close();sender.close();ClientSocket.close();} catch (Exception e) {}ExitApplication.getInstance().removeActivity(this);finish();}}@Override public void onStart() { super.onStart(); } @Override public void onResume() { super.onResume(); }@Overrideprotected void onDestroy() {super.onDestroy();}}
不得不说,我这代码里面还有两点是和zblue78大神不同的,第一个地方是这句话:dis.read(buffer,0,44),读取不需要的字节。我见过有人用28,有人用32,当然我这边测试的几台机器,包括魅族MX3,小米2S,努比亚小牛,红米NOTE,都是44的。
第二个地方,就是下面这段:
if (h264length > 50000 || h264length < 0) { while(true){byte byte1,byte2,byte3,byte4;byte1 = dis.readByte();if(byte1 == 0){byte2 = dis.readByte();if(byte2 == 0){byte3 = dis.readByte();byte4 = dis.readByte();int value; value = (int) ( ((byte1 & 0xFF)<<24) |((byte2 & 0xFF)<<16) |((byte3 & 0xFF)<<8) |(byte4 & 0xFF)); if (value > 0 && value < 50000) {h264length = value;break;}}}}
}
这一段是我后来发现,不知道为什么中间有一些杂波信号,我就把那些给滤掉了,当然也因此可能滤掉了一些正常的波,所以在播放视频的时候,偶尔会出现个一两幕马赛克供天下屌丝遐想。
其中SPS和PPS的自动获取,参看博客 http://blog.csdn.net/zgyulongfei/article/details/7538523 当然在这之前,得有个视频的预录制,就是录一小段H264码放在这,以便于获取SPS和PPS。代码如下:
public PPSandSPS(String fileName) throws IOException
{File file = new File(fileName);FileInputStream fis = new FileInputStream(file); int fileLength = (int) file.length(); byte[] fileData = new byte[fileLength]; fis.read(fileData); // 'a'=0x61, 'v'=0x76, 'c'=0x63, 'C'=0x43byte[] avcC = new byte[] { 0x61, 0x76, 0x63, 0x43 }; // avcC的起始位置 int avcRecord = 0; for (int ix = 0; ix < fileLength; ++ix) { if (fileData[ix] == avcC[0] && fileData[ix + 1] == avcC[1] && fileData[ix + 2] == avcC[2] && fileData[ix + 3] == avcC[3]) { // 找到avcC,则记录avcRecord起始位置,然后退出循环。 avcRecord = ix + 4; break; } } if (0 == avcRecord) { System.out.println("没有找到avcC,请检查文件格式是否正确"); } // 加7的目的是为了跳过 // (1)8字节的 configurationVersion // (2)8字节的 AVCProfileIndication // (3)8字节的 profile_compatibility // (4)8 字节的 AVCLevelIndication // (5)6 bit 的 reserved // (6)2 bit 的 lengthSizeMinusOne // (7)3 bit 的 reserved // (8)5 bit 的numOfSequenceParameterSets // 共6个字节,然后到达sequenceParameterSetLength的位置 int spsStartPos = avcRecord + 6; byte[] spsbt = new byte[] { fileData[spsStartPos], fileData[spsStartPos + 1] }; int spsLength = bytes2Int(spsbt); SPS = new byte[spsLength]; // 跳过2个字节的 sequenceParameterSetLength spsStartPos += 2; System.arraycopy(fileData, spsStartPos, SPS, 0, spsLength); // 底下部分为获取PPS // spsStartPos + spsLength 可以跳到pps位置 // 再加1的目的是跳过1字节的 numOfPictureParameterSets int ppsStartPos = spsStartPos + spsLength + 1; byte[] ppsbt = new byte[] { fileData[ppsStartPos], fileData[ppsStartPos + 1] }; int ppsLength = bytes2Int(ppsbt); PPS = new byte[ppsLength]; ppsStartPos += 2; System.arraycopy(fileData, ppsStartPos, PPS, 0, ppsLength);fis.close();
}
服务器转发这部分就没啥好说的了,如上所说,同0共1,收到什么转发出什么,代码如下:
public void ReceiveClient()
{byte[] buffer = new byte[4096];int lenght = 0;try{lenght = CurrentSocket.Receive(buffer, buffer.Length, SocketFlags.None);}catch (Exception){}while (lenght>0){try{opClient.CurrentSocket.Send(buffer, 0, lenght, SocketFlags.None);lenght = CurrentSocket.Receive(buffer, buffer.Length, 0);}catch (Exception){}}if (CurrentSocket.Connected){base.DisconnectSocket(CurrentSocket);}base.CloseSocket(CurrentSocket);
}
然后就是解码了。。。这方面简直殚精竭虑,却一无所获,最后不得已,从VLC下手。。。当然,前提是,我知道VLC是可以播放码流写成的文件的。这个嘛,我用的是http://vlcdotnet.codeplex.com/ 这个项目里面的控件,当然也是可以用VLC 的ACTIVEX插件的。。。但是这有一个问题,这玩意没法直接播放码流,只能播放rtp服务或者是文件,于是,我就写入临时文件,当接收到一定大小的时候,就可以开始播放了
接收:
private void RecieveAccept()
{while (true){Socket socket = ServerSocket.Accept();byte[] buffer = new byte[1024];int end = 0;try{end = socket.Receive(buffer, buffer.Length, 0);}catch (Exception){}Thread t = new Thread(Play);t.Start();while (end > 0){count += end;string filePath = AppDomain.CurrentDomain.BaseDirectory + "temp.h264";FileStream fs = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.Read);fs.Write(buffer, 0, end);fs.Flush();fs.Close();try{end = socket.Receive(buffer, buffer.Length, 0);}catch (Exception){}}socket.Disconnect(false);socket.Close();}
}
播放:
private void Play()
{while (count < 128 * 1024){Thread.Sleep(10);}PathMedia media = new PathMedia(AppDomain.CurrentDomain.BaseDirectory + "temp.h264");this.vlcControl1.Play(media);
}
其中的count是接到的数据的总长度。
到此差不多就写完了。。。应该没漏下什么吧,第一次写这么长的博客,请各位老大多多指正!要是有什么解码的方式,请指教!
Android端录制视频,.NET实时播放相关推荐
- Android MediaRecorder录制视频详细步骤
使用MediaRecorder能够编写从设备麦克风与相机捕获音视频,保存音频并(使用MediaPlayer)进行播放的应用. 1.添加权限: <uses-permission android:n ...
- 使用手机摄像头实现视频监控实时播放
使用手机摄像头实现视频监控实时播放 一.概述 视频监控实时播放的原理与目前较为流行的直播是一致的,所以采用直播的架构实现视频监控实时播放,流程图如下: #mermaid-svg-mUiqq5ywjTx ...
- 移动端加密视频的授权播放
移动端加密视频的授权播放 Polyv的移动端加密视频由hls(m3u8文件)来实现. 移动端加密视频授权播放分三个级别 1.开放授权 开放授权意味着视频可以被随意观看,视频解密的key不被保护. 2. ...
- 如何做好 Android 端音视频测试?
在用户眼中,优秀的音视频产品应该具有清晰.低延时.流畅.秒开.抗丢包.高音效等特征.为了满足用户以上要求,网易云信的工程师通过自建源站,在SDK端为了适应网络优化进行QoS优化,对视频编码器进行优化, ...
- Android开发之PCM录音实时播放的实现方法 | 边录音边播放 |PCM录音播放无延迟 | 录音无杂音 | 录音无噪音
先说下录音得开启录音权限 <uses-permission android:name="android.permission.RECORD_AUDIO" /> 然后录音 ...
- android 边录制视频边写软字幕
目前,对于边录制视频,边要显示时间戳的需求,都是通过用对应字符的bitmap图片的yuv数据,来替换每一帧yuv数据的像素点来实现的.这样做的坏处显而易见,这个时间戳数据,是硬生生的印在每一帧数据上的 ...
- Android端M3U8视频下载管理器----M3U8Manger
转载请注明出处,大力哥的博客:http://blog.csdn.net/qq137722697 M3U8Manger (M3U8管理器) M3U8Manger ,android端M3U8文件下载管理器 ...
- js前端录制视频mp4本地播放转file
前端录制视频 js export class VideoRecording { // 录视频 mediaRecorder: MediaRecorder | null; stream: MediaStr ...
- mediarecorder直播html5,html5 pc端录制视频+MediaStreamRecorder
自己花了一天研究出来的 html 5 录制视频并上传到服务器 这方面资料太少了 尤其是中文资料 借鉴 SegmentFault https://segmentfault.com/q/1010 ...
最新文章
- mysql的时间存储格式
- 体验.NET Core使用IKVM对接Java
- .NET三种异步模式(APM、EAP、TAP)
- PHPmyadmin 和 MySQL 的配置笔记
- Golang入门(4):并发
- 变形二叉树中节点的最大距离(树的最长路径)——非递归解法
- 通用数据链接(UDL)的用法
- linux如何检测文件完整,shell脚本实现linux系统文件完整性检测
- 听说你想当黑客,我只能帮你到这了
- VMware中安装linux系统(可视化界面centOS 7)
- 影视后期制作(Ae)
- 奥的斯维修服务器无响应,奥的斯GEN-2电梯故障现象:不定层的平层停梯,外呼无用断电或打检修会恢复还有运行至某层不开门自动去找平...
- chrome消除缓存的默认设置
- web前端基础案例-开发QQ空间旋转时光轴
- 【学习笔记】Android Fragments
- java 基础 api,Java基础——常用API
- mc服务器linux配置,详细教程——基于Centos搭建MC服务器(outdated)
- 2022-2028年全球与中国光谱比色计行业市场深度调研及投资预测分析
- openlayers加kriging出等值线图
- 大数据分析与应用(中级) 数据预处理与特征工程