Android5.0以上版本錄屏實現

来源:https://www.cnblogs.com/liuwa/archive/2018/01/23/8337280.html
-Advertisement-
Play Games

我錄屏的方式是分別錄製音頻和視頻,最後合併成mp4格式,比較麻煩,因為網上完整的教程比較少,所以我打算寫一個完整版的,照著我的代碼寫完之後,至少是能夠實現功能的,而不是簡單的介紹下用法。 1既然是錄製視頻,我們應該有一個按鈕控制開始和結束。 2在錄製之前,需要先判斷一下Android系統的版本是否大 ...


我錄屏的方式是分別錄製音頻和視頻,最後合併成mp4格式,比較麻煩,因為網上完整的教程比較少,所以我打算寫一個完整版的,照著我的代碼寫完之後,至少是能夠實現功能的,而不是簡單的介紹下用法。

1既然是錄製視頻,我們應該有一個按鈕控制開始和結束。

2在錄製之前,需要先判斷一下Android系統的版本是否大於5.0,並且動態申請一下許可權(讀寫,錄音,照相機),這一步可以在點開始按鈕的時候執行

    if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 102);
        }
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, 103);
        }
        if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, 104);
        }

        Intent intent = null;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            intent = mediaProjectionManager.createScreenCaptureIntent();
            startActivityForResult(intent, 101);//正常情況是要執行到這裡的,作用是申請捕捉屏幕
        } else {
            ShowUtil.showToast(context, "Android版本太低,無法使用該功能");
        }

3定義MediaProjection和MediaProjectionManager等一些其他必要的變數

  boolean isrun = false;//用來標記錄屏的狀態private MediaProjectionManager mediaProjectionManager;
    private MediaProjection mediaProjection;//錄製視頻的工具private int width, height, dpi;//屏幕寬高和dpi,後面會用到
    private ScreenRecorder screenRecorder;//這個是自己寫的錄視頻的工具類,下文會放完整的代碼
    Thread thread;//錄視頻要放線上程里去執行
在onCreat里寫好實例化
mediaProjectionManager = (MediaProjectionManager) context.getSystemService(MEDIA_PROJECTION_SERVICE);
WindowManager manager = this.getWindowManager();
DisplayMetrics outMetrics = new DisplayMetrics();
manager.getDefaultDisplay().getMetrics(outMetrics);
width = outMetrics.widthPixels;
height = outMetrics.heightPixels;
dpi = outMetrics.densityDpi;

4我們在onActivityResult回調方法中,來處理返回的事件

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == 102) {
            Toast.makeText(context, "缺少讀寫許可權", Toast.LENGTH_SHORT).show();
            return;
        }
        if (requestCode == 103) {
            Toast.makeText(context, "缺少錄音許可權", Toast.LENGTH_SHORT).show();
            return;
        }
        if (requestCode == 104) {
            Toast.makeText(context, "缺少相機許可權", Toast.LENGTH_SHORT).show();
            return;
        }
        if (requestCode != 101) {
            Log.e("HandDrawActivity", "error requestCode =" + requestCode);
        }
        if (resultCode != RESULT_OK) {
            Toast.makeText(context, "捕捉屏幕被禁止", Toast.LENGTH_SHORT).show();
            return;
        }
        
        mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
        if (mediaProjection != null) {
            screenRecorder = new ScreenRecorder(width, height, mediaProjection, dpi);
        }
        thread = new Thread() {
            @Override
            public void run() {
                screenRecorder.startRecorder();//跟ScreenRecorder有關的下文再說,總之這句話的意思就是開始錄屏的意思
            }
        };
        thread.start();
        binding.startPlayer.setText("停止");//開始和停止我用的同一個按鈕,所以開始錄屏之後把按鈕文字改一下
        isrun = true;//錄屏狀態改成真
        
    }

5先放上ScreenRecorder代碼,只想要結果的朋友呢,直接把類粘貼走,把報錯的地方改一改(在我自己的項目里可是不報錯的),就實現了錄製屏幕的功能了,還想看看的,可以往下看看

import android.hardware.display.DisplayManager;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.media.MediaRecorder;
import android.media.projection.MediaProjection;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.view.Surface;
import com.coremedia.iso.boxes.Container;
import com.googlecode.mp4parser.authoring.Movie;
import com.googlecode.mp4parser.authoring.Track;
import com.googlecode.mp4parser.authoring.builder.DefaultMp4Builder;
import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator;
import com.googlecode.mp4parser.authoring.tracks.AppendTrack;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;


public class ScreenRecorder {

    private int mWidth, mHeight, mDensty;
    private MediaProjection mediaProjection;
    private MediaCodec.BufferInfo mBufferInfo;
    private MediaCodec mEncorder;
    private Surface mInputSurface;
    private MediaMuxer mMuxer;
    private boolean isQuit = false;
    private boolean mMuxerStarted = false;
    private int mTrackIndex;
    private String path = Environment.getExternalStorageDirectory().getAbsolutePath() + "/cache";

    private MediaRecorder mediaRecorder;

    public ScreenRecorder(int mWidth, int mHeight, MediaProjection mediaProjection, int mDensty) {
        this.mWidth = mWidth;
        this.mHeight = mHeight;
        this.mediaProjection = mediaProjection;
        this.mDensty = mDensty;

        File file = new File(path);
        if (!file.exists()) {
            file.mkdirs();
        }
    }

    public void startRecorder() {
        prepareRecorder();
        startLuYin();
        startRecording();
    }

    public void stop() {
        isQuit = true;
        releaseEncorders(1);
        List<String> filePath = new ArrayList<>();
        filePath.add(path + "/APlanyinpin.amr");
        filePath.add(path + "/APlanshipin.mp4");
        joinVideo(filePath, path);
    }

    public void destory() {
        releaseEncorders(0);
    }

    private void startLuYin() {
        File file = new File(path, "APlanyinpin.amr");
        mediaRecorder = new MediaRecorder();
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
        mediaRecorder.setOutputFile(file.getAbsolutePath());

        try {
            mediaRecorder.prepare();
            mediaRecorder.start();
            Log.e("HandDrawActivity", "已經開始錄音");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void prepareRecorder() {
        mBufferInfo = new MediaCodec.BufferInfo();  //元數據,描述bytebuffer的數據,尺寸,偏移
        //創建格式化對象 MIMI_TYPE 傳入的 video/avc 是H264編碼格式
        MediaFormat format = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight);
        int frameRate = 45;
        format.setInteger(MediaFormat.KEY_BIT_RATE, 3000000);
        format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10);
        format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
        format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1);
        format.setInteger(MediaFormat.KEY_CAPTURE_RATE, frameRate);
        format.setInteger(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 1000000 / frameRate);

        try {
            mEncorder = MediaCodec.createEncoderByType("video/avc");
            mEncorder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            mInputSurface = mEncorder.createInputSurface();
            mEncorder.start();
        } catch (IOException e) {
            e.printStackTrace();
            releaseEncorders(0);
        }
    }

    private void startRecording() {
        File saveFile = new File(path, "APlanshipin.mp4");
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                mMuxer = new MediaMuxer(saveFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
                mediaProjection.createVirtualDisplay("SCREENRECORDER", mWidth, mHeight, mDensty, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
                        mInputSurface, null, null);
                drainEncoder();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    private void drainEncoder() {
        while (!isQuit) {
            Log.e("TAG", "drain.....");
            int bufferIndex = mEncorder.dequeueOutputBuffer(mBufferInfo, 0);
            if (bufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                mTrackIndex = mMuxer.addTrack(mEncorder.getOutputFormat());
                if (!mMuxerStarted && mTrackIndex >= 0) {
                    mMuxer.start();
                    mMuxerStarted = true;

                    Log.e("HandDrawActivity", "已經開始錄屏");
                }
            }
            if (bufferIndex >= 0) {
                Log.e("TAG", "drain...write..");
                ByteBuffer bufferData = mEncorder.getOutputBuffer(bufferIndex);
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    mBufferInfo.size = 0;
                }
                if (mBufferInfo.size != 0) {
                    if (mMuxerStarted) {
                        bufferData.position(mBufferInfo.offset);
                        bufferData.limit(mBufferInfo.offset + mBufferInfo.size);
                        mMuxer.writeSampleData(mTrackIndex, bufferData, mBufferInfo);
                    }
                }
                mEncorder.releaseOutputBuffer(bufferIndex, false);
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    break;
                }
            }
        }
        Log.e("HandDrawActivity", "已經結束錄屏");

    }

    private void releaseEncorders(int i) {
        if (mediaProjection != null) {
            mediaProjection.stop();
        }
        mBufferInfo = null;
        if (mEncorder != null) {
            mEncorder.stop();
        }
        mInputSurface = null;
        if (mMuxer != null && i == 1) {
            mMuxer.stop();
        }
        if (mediaRecorder != null) {
            mediaRecorder.stop();
            mediaRecorder.reset();
            mediaRecorder.release();
        }

    }

    private boolean joinVideo(List<String> filePaths, String resultPath) {
        Log.e("HandDrawActivity", "準備合成中");
        boolean result = false;

        if (filePaths == null || filePaths.size() <= 0 || TextUtils.isEmpty(resultPath)) {
            throw new IllegalArgumentException();
        }

        if (filePaths.size() == 1) { // 只有一個視頻片段,不需要合併
            return true;
        }

        try {
            Movie[] inMovies = new Movie[filePaths.size()];
            for (int i = 0; i < filePaths.size(); i++) {
                Log.e("HandDrawActivity", "filePaths=" + filePaths.get(i));
                File f = new File(filePaths.get(i));
                if (f.exists()) {
                    inMovies[i] = MovieCreator.build(filePaths.get(i));
                }
            }

            // 分別取出音軌和視頻
            List<Track> videoTracks = new LinkedList<>();
            List<Track> audioTracks = new LinkedList<>();
            for (Movie m : inMovies) {
                for (Track t : m.getTracks()) {
                    if (t.getHandler().equals("soun")) {
                        audioTracks.add(t);
                    }
                    if (t.getHandler().equals("vide")) {
                        videoTracks.add(t);
                    }
                }
            }

            // 合併到最終的視頻文件
            Movie outMovie = new Movie();

            if (audioTracks.size() > 0) {
                outMovie.addTrack(new AppendTrack(audioTracks.toArray(new Track[audioTracks.size()])));
            }
            if (videoTracks.size() > 0) {
                outMovie.addTrack(new AppendTrack(videoTracks.toArray(new Track[videoTracks.size()])));
            }

            Container mp4file = new DefaultMp4Builder().build(outMovie);

            // 將文件輸出
            File resultFile = new File(resultPath, "APlanTeacherAnswer.mp4");
            if (resultFile.exists() && resultFile.isFile()) {
                resultFile.delete();
            }
            FileChannel fc = new RandomAccessFile(resultFile, "rw").getChannel();
            mp4file.writeContainer(fc);
            fc.close();
            Log.e("HandDrawActivity", "合成完畢");
            // 合成完成後把原片段文件刪除
            for (String filePath : filePaths) {
                File file = new File(filePath);
                file.delete();
            }
            result = true;

            HandDrawActivity.sendVideo();

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;

    }
}

6從startRecorder方法說起

public void startRecorder() {
        prepareRecorder();//錄視頻前的準備
        startLuYin();//直接錄音頻(不用準備)
        startRecording();//錄視頻
    }
錄音的方法
private
void startLuYin() { File file = new File(path, "APlanyinpin.amr"); mediaRecorder = new MediaRecorder(); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//聲音來源,麥克 mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);//音頻格式,預設,其實就是上面定義好的amr了,除此之外還有mp4 mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);//編碼格式,問題是我不知道編碼格式對什麼有影響,是音質高低還是文件大小還是解析快慢,等我有時間去專門研究一下 mediaRecorder.setOutputFile(file.getAbsolutePath()); try { mediaRecorder.prepare(); mediaRecorder.start(); Log.e("HandDrawActivity", "已經開始錄音"); } catch (IOException e) { e.printStackTrace(); } }
//錄視頻前的準備工作
private
void prepareRecorder() { mBufferInfo = new MediaCodec.BufferInfo(); //元數據,描述bytebuffer的數據,尺寸,偏移 //創建格式化對象 MIMI_TYPE 傳入的 video/avc 是H264編碼格式 MediaFormat format = MediaFormat.createVideoFormat("video/avc", mWidth, mHeight); int frameRate = 45; format.setInteger(MediaFormat.KEY_BIT_RATE, 3000000); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10); format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); format.setInteger(MediaFormat.KEY_CAPTURE_RATE, frameRate); format.setInteger(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, 1000000 / frameRate);//編碼器的設置,具體是設置的啥我也不太清楚,但是網上查一查都是這麼寫的!!! try { mEncorder = MediaCodec.createEncoderByType("video/avc"); mEncorder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mInputSurface = mEncorder.createInputSurface(); mEncorder.start();//讓編碼器先跑起來 } catch (IOException e) { e.printStackTrace(); releaseEncorders(0); } }
這裡也是準備工作
private
void startRecording() { File saveFile = new File(path, "APlanshipin.mp4"); try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mMuxer = new MediaMuxer(saveFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);//百度一下MediaMuxer,講的很詳細的 mediaProjection.createVirtualDisplay("SCREENRECORDER", mWidth, mHeight, mDensty, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mInputSurface, null, null); drainEncoder(); } } catch (Exception e) { e.printStackTrace(); } }
這個就是開始寫視頻文件了
private
void drainEncoder() { while (!isQuit) { Log.e("TAG", "drain....."); int bufferIndex = mEncorder.dequeueOutputBuffer(mBufferInfo, 0); if (bufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { mTrackIndex = mMuxer.addTrack(mEncorder.getOutputFormat()); if (!mMuxerStarted && mTrackIndex >= 0) { mMuxer.start(); mMuxerStarted = true; Log.e("HandDrawActivity", "已經開始錄屏"); } } if (bufferIndex >= 0) { Log.e("TAG", "drain...write.."); ByteBuffer bufferData = mEncorder.getOutputBuffer(bufferIndex); if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { mBufferInfo.size = 0; } if (mBufferInfo.size != 0) { if (mMuxerStarted) { bufferData.position(mBufferInfo.offset); bufferData.limit(mBufferInfo.offset + mBufferInfo.size); mMuxer.writeSampleData(mTrackIndex, bufferData, mBufferInfo); } } mEncorder.releaseOutputBuffer(bufferIndex, false); if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { break; } } } Log.e("HandDrawActivity", "已經結束錄屏"); }
這個就是把錄好的音頻和視頻合併成mp4的方法了,也是點擊停止錄屏的時候用到的
private
boolean joinVideo(List<String> filePaths, String resultPath) { Log.e("HandDrawActivity", "準備合成中"); boolean result = false; if (filePaths == null || filePaths.size() <= 0 || TextUtils.isEmpty(resultPath)) { throw new IllegalArgumentException(); } if (filePaths.size() == 1) { // 只有一個視頻片段,不需要合併 return true; } try { Movie[] inMovies = new Movie[filePaths.size()]; for (int i = 0; i < filePaths.size(); i++) { Log.e("HandDrawActivity", "filePaths=" + filePaths.get(i)); File f = new File(filePaths.get(i)); if (f.exists()) { inMovies[i] = MovieCreator.build(filePaths.get(i)); } } // 分別取出音軌和視頻 List<Track> videoTracks = new LinkedList<>(); List<Track> audioTracks = new LinkedList<>(); for (Movie m : inMovies) { for (Track t : m.getTracks()) { if (t.getHandler().equals("soun")) { audioTracks.add(t); } if (t.getHandler().equals("vide")) { videoTracks.add(t); } } } // 合併到最終的視頻文件 Movie outMovie = new Movie(); if (audioTracks.size() > 0) { outMovie.addTrack(new AppendTrack(audioTracks.toArray(new Track[audioTracks.size()]))); } if (videoTracks.size() > 0) { outMovie.addTrack(new AppendTrack(videoTracks.toArray(new Track[videoTracks.size()]))); } Container mp4file = new DefaultMp4Builder().build(outMovie); // 將文件輸出 File resultFile = new File(resultPath, "APlanTeacherAnswer.mp4"); if (resultFile.exists() && resultFile.isFile()) { resultFile.delete(); } FileChannel fc = new RandomAccessFile(resultFile, "rw").getChannel(); mp4file.writeContainer(fc); fc.close(); Log.e("HandDrawActivity", "合成完畢"); // 合成完成後把原片段文件刪除 for (String filePath : filePaths) { File file = new File(filePath); file.delete(); } result = true; HandDrawActivity.sendVideo(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } return result; }
這個就是結束的時候了,該清空的清空,該註銷的註銷, i是用來判斷錄沒錄的,有可能剛進入這個頁面都沒錄過,直接就返回到別的頁面了,那就有可能空指針異常,因為有些變數都沒初始化,所以用i判斷一下,也可以自己寫別的方法判端
private
void releaseEncorders(int i) { if (mediaProjection != null) { mediaProjection.stop(); } mBufferInfo = null; if (mEncorder != null) { mEncorder.stop(); } mInputSurface = null; if (mMuxer != null && i == 1) { mMuxer.stop(); } if (mediaRecorder != null) { mediaRecorder.stop(); mediaRecorder.reset(); mediaRecorder.release(); } }

7部分代碼也是我從網上扒的,但是網上的代碼就沒怎麼見過比較完整的版本的,我上面寫的都是經過我自己測試絕對沒問題的而且代碼也沒什麼遺漏的,要是發現有遺漏的代碼我後續再補上。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Connected to: Oracle Database 10g Enterprise Edition Release 10.2.0.4.0 - 64bit ProductionWith the Partitioning, OLAP, Data Mining and Real Applicatio ...
  • 後端開發:1、高級java軟體架構師實戰培訓視頻教程2、大型SpringMVC,Mybatis,Redis,Solr,Nginx,SSM分散式電商項目視頻教程3、Spark Streaming實時流處理項目實戰4、Java校招面試 Google面試官親授5、Java開發企業級許可權管理系統6、Java ...
  • 1、設置密碼 set password for 用戶名@localhost = password('密碼'); 2、取消密碼 set password for 用戶名@localhost = password(''); 3、創建賬號 create user admin identified by ' ...
  • wm_concat(列名)函數,能把指定的列的值,(按照group by 中指定的分隔方法),一個個用逗號鏈接起來,例: shopping表: id goods num 1 蘋果 22 梨子 32 西瓜 41 葡萄 53 香蕉 63 橘子 7 select id,wm_concat(goods) f ...
  • sql server2008資料庫複製實現數據同步常見問題 在原作者基礎上追加 "sql server2008資料庫複製實現數據同步常見問題" 23.發佈 'xx' 的併發快照不可用,因為該快照尚未完全生成,或者日誌讀取器代理未運行,無法激活它。如果併發快照的生成過程中斷,則必須重新啟動用於該發佈的 ...
  • 在mysql5.1以後開始支持事件功能,主要是定期或指定的時間執行一條命令。 mysql預設是關閉事件功能 檢查是否開啟事件功能: show variables llike 'event_scheduler'; 開啟事件: set global event_scheduler = on; 關閉事件: ...
  • SQL的不同版本在Windows環境啟動配置方法不同,此處僅介紹 5.7.20的配置方法; 1、登錄mysql官網下載windows環境下的工具壓縮包 http://dev.mysql.com/downloads/mysql/ 個人電腦是64位的機器,所以此處演示選擇如下的版本 2、解壓到本地電腦的 ...
  • 1.創建函數 delimiter // create function function_name(形參名 數據類型) returns 數據類型 #必須指定返回的數據類型。 begin stmt; end // delimiter ; 2.刪除函數 drop function function_na ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...