通过 WebRTC 共享屏幕很容易

时间:2021-7-3 作者:qvyue

简介

网络会议中常用的屏幕共享功能使用
WebRTC 提供的 getDisplayMedia API 就能轻松实现,接口如下

var promise = navigator.mediaDevices.getDisplayMedia(constraints);

MediaDevices 接口的 getDisplayMedia 方法提示用户选择并授予将显示屏幕或其部分(如浏览器窗口和标签页)的内容捕获为 MediaStream 的权限。然后,可以使用媒体流录制 API 录制, 或作为 WebRTC 会话的一部分传输所生成的媒体流。这个媒体流传输到网络的对端,这样就可以共享屏幕的内容了。

例如

async function startCapture(displayMediaOptions) {
  let captureStream = null;

  try {
    captureStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
  } catch(err) {
    console.error("Error: " + err);
  }
  return captureStream;
}

示例1

通过 WebRTC 共享屏幕很容易
  • HTML 文件






Click the button to open or close connection



  • JS 文件
'use strict';

// Polyfill in Firefox.
// See https://blog.mozilla.org/webrtc/getdisplaymedia-now-available-in-adapter-js/
if (adapter.browserDetails.browser == 'firefox') {
  adapter.browserShim.shimGetDisplayMedia(window, 'screen');
}

const gdmOptions = {
  video: {
    cursor: "always"
  },
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    sampleRate: 44100
  }
}

function handleSuccess(stream) {
  startButton.disabled = true;
  const video = document.querySelector('video');
  video.srcObject = stream;

  // demonstrates how to detect that the user has stopped
  // sharing the screen via the browser UI.
  stream.getVideoTracks()[0].addEventListener('ended', () => {
    errorMsg('The user has ended sharing the screen');
    startButton.disabled = false;
  });
}

function handleError(error) {
  errorMsg(`getDisplayMedia error: ${error.name}`, error);
}

function errorMsg(msg, error) {
  const errorElement = document.querySelector('#errorMsg');
  errorElement.innerHTML += `

${msg}

`; if (typeof error !== 'undefined') { console.error(error); } } const startButton = document.getElementById('startButton'); startButton.addEventListener('click', () => { navigator.mediaDevices.getDisplayMedia(gdmOptions) .then(handleSuccess, handleError); }); if ((navigator.mediaDevices && 'getDisplayMedia' in navigator.mediaDevices)) { startButton.disabled = false; } else { errorMsg('getDisplayMedia is not supported'); }

测试

点击 share 按键,可以选择 为文本 还是 运动共享,后者的帧率会高点。然后再选择屏幕,应用或标签页。

示例2

服务器端

使用 nodejs + socket.io 充当web服务器,并用来传递 sdp

代码:https://github.com/walterfan/webrtc_primer/blob/main/examples/screen_share_server.js

启动命令

node screen_share_server.js
[2021-04-21T20:18:41.018] [INFO] screen_share - screen shares server listen on https://localhost:8183

客户端还是 html + JavaScript

详细代码:

  • https://github.com/walterfan/webrtc_primer/blob/main/examples/screen_share_demo.html
  • https://github.com/walterfan/webrtc_primer/blob/main/examples/js/screen_share_client.js

测试

将 getDisplayMedia 得到的媒体流 MediaStream 通过 PeerConnection 传送给对方

通过 WebRTC 共享屏幕很容易
  1. 张三进入会议室 "Join room"
  2. 李四进入会议室 "Join room"
  3. 张三分享屏幕 "start share"
通过 WebRTC 共享屏幕很容易
本地屏幕
  1. 李四就会看到分享的屏幕内容
通过 WebRTC 共享屏幕很容易
远端屏幕

注意我们需要观察 RTP 包的荷载内容,所以要关掉 SRTP ,只用 RTP 来传输
出于测试目的, Chrome Canary 及 Chrome Developer 有一个选项 --disable-webrtc-encryption 可以关掉 SRTP

在我的 macbook 上步骤如下:

cd /Applications/Google Chrome Canary.app/Contents/MacOS/
./Google Chrome Canary --disable-webrtc-encryption

于是在创建 RTP 连接时的 SDP 就从:

m=video 9 RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116

就变成了

m=video 9 RTP/AVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 35 36 124 119 123 118 114 115 116

完整sdp如下

  • offer sdp 消息:
{
  type: 'offer',
  sdp: 'v=0rn' +
    'o=- 2151254633287699884 2 IN IP4 127.0.0.1rn' +
    's=-rn' +
    't=0 0rn' +
    'a=group:BUNDLE 0 1rn' +
    'a=extmap-allow-mixedrn' +
    'a=msid-semantic: WMS VLvw0Ec4NwiXKVzTZyzl1m5aSLGW9EPe50Mzrn' +
    'm=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126rn' +
    'c=IN IP4 0.0.0.0rn' +
    'a=rtcp:9 IN IP4 0.0.0.0rn' +
    'a=ice-ufrag:wkafrn' +
    'a=ice-pwd:2PI01Rh/wf4JKpfM0pr6LJ+drn' +
    'a=ice-options:tricklern' +
    'a=fingerprint:sha-256 86:BC:0B:F2:AB:2F:A2:A0:7F:FC:5B:5E:16:B8:61:62:E6:E6:18:FF:B6:85:6C:F0:DD:65:01:72:C1:16:88:E8rn' +
    'a=setup:actpassrn' +
    'a=mid:0rn' +
    'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-levelrn' +
    'a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timern' +
    'a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01rn' +
    'a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:midrn' +
    'a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-idrn' +
    'a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-idrn' +
    'a=sendrecvrn' +
    'a=msid:VLvw0Ec4NwiXKVzTZyzl1m5aSLGW9EPe50Mz cd1c92ae-7d05-4ee8-9e21-3e2993c1c254rn' +
    'a=rtcp-muxrn' +
    'a=rtpmap:111 opus/48000/2rn' +
    'a=rtcp-fb:111 transport-ccrn' +
    'a=fmtp:111 minptime=10;useinbandfec=1rn' +
    'a=rtpmap:103 ISAC/16000rn' +
    'a=rtpmap:104 ISAC/32000rn' +
    'a=rtpmap:9 G722/8000rn' +
    'a=rtpmap:0 PCMU/8000rn' +
    'a=rtpmap:8 PCMA/8000rn' +
    'a=rtpmap:106 CN/32000rn' +
    'a=rtpmap:105 CN/16000rn' +
    'a=rtpmap:13 CN/8000rn' +
    'a=rtpmap:110 telephone-event/48000rn' +
    'a=rtpmap:112 telephone-event/32000rn' +
    'a=rtpmap:113 telephone-event/16000rn' +
    'a=rtpmap:126 telephone-event/8000rn' +
    'a=ssrc:3060416220 cname:C9N45apy7vfT4Waqrn' +
    'a=ssrc:3060416220 msid:VLvw0Ec4NwiXKVzTZyzl1m5aSLGW9EPe50Mz cd1c92ae-7d05-4ee8-9e21-3e2993c1c254rn' +
    'a=ssrc:3060416220 mslabel:VLvw0Ec4NwiXKVzTZyzl1m5aSLGW9EPe50Mzrn' +
    'a=ssrc:3060416220 label:cd1c92ae-7d05-4ee8-9e21-3e2993c1c254rn' +
    'm=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 124 119 123 118 114 115 116rn' +
    'c=IN IP4 0.0.0.0rn' +
    'a=rtcp:9 IN IP4 0.0.0.0rn' +
    'a=ice-ufrag:wkafrn' +
    'a=ice-pwd:2PI01Rh/wf4JKpfM0pr6LJ+drn' +
    'a=ice-options:tricklern' +
    'a=fingerprint:sha-256 86:BC:0B:F2:AB:2F:A2:A0:7F:FC:5B:5E:16:B8:61:62:E6:E6:18:FF:B6:85:6C:F0:DD:65:01:72:C1:16:88:E8rn' +
    'a=setup:actpassrn' +
    'a=mid:1rn' +
    'a=extmap:14 urn:ietf:params:rtp-hdrext:toffsetrn' +
    'a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timern' +
    'a=extmap:13 urn:3gpp:video-orientationrn' +
    'a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01rn' +
    'a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delayrn' +
    'a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-typern' +
    'a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timingrn' +
    'a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-spacern' +
    'a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:midrn' +
    'a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-idrn' +
    'a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-idrn' +
    'a=sendrecvrn' +
    'a=msid:VLvw0Ec4NwiXKVzTZyzl1m5aSLGW9EPe50Mz 95173d41-de5c-4864-8745-a8573f23f3d8rn' +
    'a=rtcp-muxrn' +
    'a=rtcp-rsizern' +
    'a=rtpmap:102 H264/90000rn' +
    'a=rtcp-fb:102 goog-rembrn' +
    'a=rtcp-fb:102 transport-ccrn' +
    'a=rtcp-fb:102 ccm firrn' +
    'a=rtcp-fb:102 nackrn' +
    'a=rtcp-fb:102 nack plirn' +
    'a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001frn' +
    'a=rtpmap:121 rtx/90000rn' +
    'a=fmtp:121 apt=102rn' +
    'a=rtpmap:127 H264/90000rn' +
    'a=rtcp-fb:127 goog-rembrn' +
    'a=rtcp-fb:127 transport-ccrn' +
    'a=rtcp-fb:127 ccm firrn' +
    'a=rtcp-fb:127 nackrn' +
    'a=rtcp-fb:127 nack plirn' +
    'a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001frn' +
    'a=rtpmap:120 rtx/90000rn' +
    'a=fmtp:120 apt=127rn' +
    'a=rtpmap:125 H264/90000rn' +
    'a=rtcp-fb:125 goog-rembrn' +
    'a=rtcp-fb:125 transport-ccrn' +
    'a=rtcp-fb:125 ccm firrn' +
    'a=rtcp-fb:125 nackrn' +
    'a=rtcp-fb:125 nack plirn' +
    'a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01frn' +
    'a=rtpmap:107 rtx/90000rn' +
    'a=fmtp:107 apt=125rn' +
    'a=rtpmap:108 H264/90000rn' +
    'a=rtcp-fb:108 goog-rembrn' +
    'a=rtcp-fb:108 transport-ccrn' +
    'a=rtcp-fb:108 ccm firrn' +
    'a=rtcp-fb:108 nackrn' +
    'a=rtcp-fb:108 nack plirn' +
    'a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01frn' +
    'a=rtpmap:109 rtx/90000rn' +
    'a=fmtp:109 apt=108rn' +
    'a=rtpmap:124 H264/90000rn' +
    'a=rtcp-fb:124 goog-rembrn' +
    'a=rtcp-fb:124 transport-ccrn' +
    'a=rtcp-fb:124 ccm firrn' +
    'a=rtcp-fb:124 nackrn' +
    'a=rtcp-fb:124 nack plirn' +
    'a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d0032rn' +
    'a=rtpmap:119 rtx/90000rn' +
    'a=fmtp:119 apt=124rn' +
    'a=rtpmap:123 H264/90000rn' +
    'a=rtcp-fb:123 goog-rembrn' +
    'a=rtcp-fb:123 transport-ccrn' +
    'a=rtcp-fb:123 ccm firrn' +
    'a=rtcp-fb:123 nackrn' +
    'a=rtcp-fb:123 nack plirn' +
    'a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032rn' +
    'a=rtpmap:118 rtx/90000rn' +
    'a=fmtp:118 apt=123rn' +
    'a=rtpmap:114 red/90000rn' +
    'a=rtpmap:115 rtx/90000rn' +
    'a=fmtp:115 apt=114rn' +
    'a=rtpmap:116 ulpfec/90000rn' +
    'a=ssrc-group:FID 3194951553 658670364rn' +
    'a=ssrc:3194951553 cname:C9N45apy7vfT4Waqrn' +
    'a=ssrc:3194951553 msid:VLvw0Ec4NwiXKVzTZyzl1m5aSLGW9EPe50Mz 95173d41-de5c-4864-8745-a8573f23f3d8rn' +
    'a=ssrc:3194951553 mslabel:VLvw0Ec4NwiXKVzTZyzl1m5aSLGW9EPe50Mzrn' +
    'a=ssrc:3194951553 label:95173d41-de5c-4864-8745-a8573f23f3d8rn' +
    'a=ssrc:658670364 cname:C9N45apy7vfT4Waqrn' +
    'a=ssrc:658670364 msid:VLvw0Ec4NwiXKVzTZyzl1m5aSLGW9EPe50Mz 95173d41-de5c-4864-8745-a8573f23f3d8rn' +
    'a=ssrc:658670364 mslabel:VLvw0Ec4NwiXKVzTZyzl1m5aSLGW9EPe50Mzrn' +
    'a=ssrc:658670364 label:95173d41-de5c-4864-8745-a8573f23f3d8rn'
}

  • Answer SDP 消息

 {
  type: 'answer',
  sdp: 'v=0rn' +
    'o=- 6808421739235470893 2 IN IP4 127.0.0.1rn' +
    's=-rn' +
    't=0 0rn' +
    'a=group:BUNDLE 0 1rn' +
    'a=extmap-allow-mixedrn' +
    'a=msid-semantic: WMSrn' +
    'm=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126rn' +
    'c=IN IP4 0.0.0.0rn' +
    'a=rtcp:9 IN IP4 0.0.0.0rn' +
    'a=ice-ufrag:8fbZrn' +
    'a=ice-pwd:r/ZnPQzn6uh8LIKW1gfaacu6rn' +
    'a=ice-options:tricklern' +
    'a=fingerprint:sha-256 3A:5E:40:E4:BD:31:64:74:86:41:5A:62:1B:CA:0A:0A:4A:A4:0D:59:68:D5:47:15:B6:53:FE:BE:0F:3C:8D:D6rn' +
    'a=setup:activern' +
    'a=mid:0rn' +
    'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-levelrn' +
    'a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timern' +
    'a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01rn' +
    'a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:midrn' +
    'a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-idrn' +
    'a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-idrn' +
    'a=recvonlyrn' +
    'a=rtcp-muxrn' +
    'a=rtpmap:111 opus/48000/2rn' +
    'a=rtcp-fb:111 transport-ccrn' +
    'a=fmtp:111 minptime=10;useinbandfec=1rn' +
    'a=rtpmap:103 ISAC/16000rn' +
    'a=rtpmap:104 ISAC/32000rn' +
    'a=rtpmap:9 G722/8000rn' +
    'a=rtpmap:0 PCMU/8000rn' +
    'a=rtpmap:8 PCMA/8000rn' +
    'a=rtpmap:106 CN/32000rn' +
    'a=rtpmap:105 CN/16000rn' +
    'a=rtpmap:13 CN/8000rn' +
    'a=rtpmap:110 telephone-event/48000rn' +
    'a=rtpmap:112 telephone-event/32000rn' +
    'a=rtpmap:113 telephone-event/16000rn' +
    'a=rtpmap:126 telephone-event/8000rn' +
    'm=video 9 UDP/TLS/RTP/SAVPF 102 121 127 120 125 107 108 109 124 119 123 118 114 115 116rn' +
    'c=IN IP4 0.0.0.0rn' +
    'a=rtcp:9 IN IP4 0.0.0.0rn' +
    'a=ice-ufrag:8fbZrn' +
    'a=ice-pwd:r/ZnPQzn6uh8LIKW1gfaacu6rn' +
    'a=ice-options:tricklern' +
    'a=fingerprint:sha-256 3A:5E:40:E4:BD:31:64:74:86:41:5A:62:1B:CA:0A:0A:4A:A4:0D:59:68:D5:47:15:B6:53:FE:BE:0F:3C:8D:D6rn' +
    'a=setup:activern' +
    'a=mid:1rn' +
    'a=extmap:14 urn:ietf:params:rtp-hdrext:toffsetrn' +
    'a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-timern' +
    'a=extmap:13 urn:3gpp:video-orientationrn' +
    'a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01rn' +
    'a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delayrn' +
    'a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-typern' +
    'a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timingrn' +
    'a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-spacern' +
    'a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:midrn' +
    'a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-idrn' +
    'a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-idrn' +
    'a=recvonlyrn' +
    'a=rtcp-muxrn' +
    'a=rtcp-rsizern' +
    'a=rtpmap:102 H264/90000rn' +
    'a=rtcp-fb:102 goog-rembrn' +
    'a=rtcp-fb:102 transport-ccrn' +
    'a=rtcp-fb:102 ccm firrn' +
    'a=rtcp-fb:102 nackrn' +
    'a=rtcp-fb:102 nack plirn' +
    'a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001frn' +
    'a=rtpmap:121 rtx/90000rn' +
    'a=fmtp:121 apt=102rn' +
    'a=rtpmap:127 H264/90000rn' +
    'a=rtcp-fb:127 goog-rembrn' +
    'a=rtcp-fb:127 transport-ccrn' +
    'a=rtcp-fb:127 ccm firrn' +
    'a=rtcp-fb:127 nackrn' +
    'a=rtcp-fb:127 nack plirn' +
    'a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001frn' +
    'a=rtpmap:120 rtx/90000rn' +
    'a=fmtp:120 apt=127rn' +
    'a=rtpmap:125 H264/90000rn' +
    'a=rtcp-fb:125 goog-rembrn' +
    'a=rtcp-fb:125 transport-ccrn' +
    'a=rtcp-fb:125 ccm firrn' +
    'a=rtcp-fb:125 nackrn' +
    'a=rtcp-fb:125 nack plirn' +
    'a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01frn' +
    'a=rtpmap:107 rtx/90000rn' +
    'a=fmtp:107 apt=125rn' +
    'a=rtpmap:108 H264/90000rn' +
    'a=rtcp-fb:108 goog-rembrn' +
    'a=rtcp-fb:108 transport-ccrn' +
    'a=rtcp-fb:108 ccm firrn' +
    'a=rtcp-fb:108 nackrn' +
    'a=rtcp-fb:108 nack plirn' +
    'a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01frn' +
    'a=rtpmap:109 rtx/90000rn' +
    'a=fmtp:109 apt=108rn' +
    'a=rtpmap:124 H264/90000rn' +
    'a=rtcp-fb:124 goog-rembrn' +
    'a=rtcp-fb:124 transport-ccrn' +
    'a=rtcp-fb:124 ccm firrn' +
    'a=rtcp-fb:124 nackrn' +
    'a=rtcp-fb:124 nack plirn' +
    'a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d0015rn' +
    'a=rtpmap:119 rtx/90000rn' +
    'a=fmtp:119 apt=124rn' +
    'a=rtpmap:123 H264/90000rn' +
    'a=rtcp-fb:123 goog-rembrn' +
    'a=rtcp-fb:123 transport-ccrn' +
    'a=rtcp-fb:123 ccm firrn' +
    'a=rtcp-fb:123 nackrn' +
    'a=rtcp-fb:123 nack plirn' +
    'a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640015rn' +
    'a=rtpmap:118 rtx/90000rn' +
    'a=fmtp:118 apt=123rn' +
    'a=rtpmap:114 red/90000rn' +
    'a=rtpmap:115 rtx/90000rn' +
    'a=fmtp:115 apt=114rn' +
    'a=rtpmap:116 ulpfec/90000rn'
}

Wireshark 抓包

1.首先安装wireshark软件,这个地球人都知道

2.用wireshark抓取H264视频码流,最好过滤掉其他码流

3.右键点击H264的udp包,选择"Decode as...",再选择Transport中的rtp选项,就解析成rtp包了

4.查看rtp包的payload type,比如说type是 102,那么在wireshark工具栏选择Edit->preferences->protocols->H264, 把H264 dynamic payload types设成 102

  • RTP 流
通过 WebRTC 共享屏幕很容易
  • RTP 包

    通过 WebRTC 共享屏幕很容易
  • H.264 荷载

  1. SPS
通过 WebRTC 共享屏幕很容易
  1. FU-A
通过 WebRTC 共享屏幕很容易

推荐阅读更多精彩内容

  • WebRTC1.0 浏览器间的实时通信
    什么是WebRTC WebRTC即Web Real-Time Communication(网页实时通信)的缩写,是...
    网易云信阅读 1,019评论 0赞 0
  • WebRTC学习笔记七 pion/webrtc
    https://github.com/pion/webrtc[https://github.com/pion/we...
    合肥黑阅读 472评论 0赞 0
  • webrtc源码之nack&&rtx详解
    1、nack协商 m=video 9 RTP/AVPF 96 97 98 99 100 101127 122 10...
    和松阅读 2,762评论 3赞 2
  • 2018-7-18,晚上,7:00-8:30,注音标3-1-3,口语滚动复习。
    xin_8008阅读 2,314评论 0赞 3
  • 有效教学(摘记八)——崔允漷
    第八章 教学评价 第一节 从考试文化走向评价文化 一、教学评价的早期发展 (一)传统考试阶段 ★《学记》——我国最...
    Dreamerr__阅读 1,861评论 1赞 5
评论0
抽奖

reward

1赞
赞赏
更多好文

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。