gemercheung 3 лет назад
Родитель
Сommit
564e88b592
100 измененных файлов с 426 добавлено и 15804 удалено
  1. 1 3
      libs/utils/src/utils.module.ts
  2. 0 15753
      package-lock.json
  3. 2 0
      package.json
  4. 1 0
      src/main.ts
  5. 185 4
      src/meta.gateway.ts
  6. 186 32
      src/ws-adapter.ts
  7. 27 0
      ws/HeartBeat.js
  8. 24 0
      ws/index.html
  9. 0 12
      ws/test.html
  10. BIN
      ws/video/earth/0
  11. BIN
      ws/video/earth/1
  12. BIN
      ws/video/earth/10
  13. BIN
      ws/video/earth/100
  14. BIN
      ws/video/earth/101
  15. BIN
      ws/video/earth/102
  16. BIN
      ws/video/earth/103
  17. BIN
      ws/video/earth/104
  18. BIN
      ws/video/earth/105
  19. BIN
      ws/video/earth/106
  20. BIN
      ws/video/earth/107
  21. BIN
      ws/video/earth/108
  22. BIN
      ws/video/earth/109
  23. BIN
      ws/video/earth/11
  24. BIN
      ws/video/earth/110
  25. BIN
      ws/video/earth/111
  26. BIN
      ws/video/earth/112
  27. BIN
      ws/video/earth/113
  28. BIN
      ws/video/earth/114
  29. BIN
      ws/video/earth/115
  30. BIN
      ws/video/earth/116
  31. BIN
      ws/video/earth/117
  32. BIN
      ws/video/earth/118
  33. BIN
      ws/video/earth/119
  34. BIN
      ws/video/earth/12
  35. BIN
      ws/video/earth/120
  36. BIN
      ws/video/earth/121
  37. BIN
      ws/video/earth/122
  38. BIN
      ws/video/earth/123
  39. BIN
      ws/video/earth/124
  40. BIN
      ws/video/earth/125
  41. BIN
      ws/video/earth/126
  42. BIN
      ws/video/earth/127
  43. BIN
      ws/video/earth/128
  44. BIN
      ws/video/earth/129
  45. BIN
      ws/video/earth/13
  46. BIN
      ws/video/earth/130
  47. BIN
      ws/video/earth/131
  48. BIN
      ws/video/earth/132
  49. BIN
      ws/video/earth/133
  50. BIN
      ws/video/earth/134
  51. BIN
      ws/video/earth/135
  52. BIN
      ws/video/earth/136
  53. BIN
      ws/video/earth/137
  54. BIN
      ws/video/earth/138
  55. BIN
      ws/video/earth/139
  56. BIN
      ws/video/earth/14
  57. BIN
      ws/video/earth/140
  58. BIN
      ws/video/earth/141
  59. BIN
      ws/video/earth/142
  60. BIN
      ws/video/earth/143
  61. BIN
      ws/video/earth/144
  62. BIN
      ws/video/earth/145
  63. BIN
      ws/video/earth/146
  64. BIN
      ws/video/earth/147
  65. BIN
      ws/video/earth/148
  66. BIN
      ws/video/earth/149
  67. BIN
      ws/video/earth/15
  68. BIN
      ws/video/earth/150
  69. BIN
      ws/video/earth/151
  70. BIN
      ws/video/earth/152
  71. BIN
      ws/video/earth/153
  72. BIN
      ws/video/earth/154
  73. BIN
      ws/video/earth/155
  74. BIN
      ws/video/earth/156
  75. BIN
      ws/video/earth/157
  76. BIN
      ws/video/earth/158
  77. BIN
      ws/video/earth/159
  78. BIN
      ws/video/earth/16
  79. BIN
      ws/video/earth/160
  80. BIN
      ws/video/earth/161
  81. BIN
      ws/video/earth/162
  82. BIN
      ws/video/earth/163
  83. BIN
      ws/video/earth/164
  84. BIN
      ws/video/earth/165
  85. BIN
      ws/video/earth/166
  86. BIN
      ws/video/earth/167
  87. BIN
      ws/video/earth/168
  88. BIN
      ws/video/earth/169
  89. BIN
      ws/video/earth/17
  90. BIN
      ws/video/earth/170
  91. BIN
      ws/video/earth/171
  92. BIN
      ws/video/earth/172
  93. BIN
      ws/video/earth/173
  94. BIN
      ws/video/earth/174
  95. BIN
      ws/video/earth/175
  96. BIN
      ws/video/earth/176
  97. BIN
      ws/video/earth/177
  98. BIN
      ws/video/earth/178
  99. BIN
      ws/video/earth/179
  100. 0 0
      ws/video/earth/18

+ 1 - 3
libs/utils/src/utils.module.ts

@@ -5,6 +5,4 @@ import { UtilsService } from './utils.service';
   providers: [UtilsService],
   exports: [UtilsService],
 })
-export class UtilsModule {
-
-}
+export class UtilsModule {}

Разница между файлами не показана из-за своего большого размера
+ 0 - 15753
package-lock.json


+ 2 - 0
package.json

@@ -27,6 +27,8 @@
     "@nestjs/platform-socket.io": "^8.4.4",
     "@nestjs/platform-ws": "^8.4.4",
     "@nestjs/websockets": "^8.4.4",
+    "buffer": "^6.0.3",
+    "node-datachannel": "^0.3.1",
     "reflect-metadata": "^0.1.13",
     "rimraf": "^3.0.2",
     "rxjs": "^7.2.0"

+ 1 - 0
src/main.ts

@@ -1,5 +1,6 @@
 import { NestFactory } from '@nestjs/core';
 import { AppModule } from './app.module';
+// import { WsAdapter } from '@nestjs/platform-ws';
 import { WsAdapter } from './ws-adapter';
 import { Logger } from '@nestjs/common';
 

+ 185 - 4
src/meta.gateway.ts

@@ -6,10 +6,17 @@ import {
   OnGatewayConnection,
   OnGatewayDisconnect,
 } from '@nestjs/websockets';
+
 import { Server } from 'ws';
 import * as WebSocket from 'ws';
 
+import { PeerConnection, initLogger, DataChannel } from 'node-datachannel';
+import { Buffer } from 'buffer';
 import { Logger } from '@nestjs/common';
+import * as path from 'path';
+import { createReadStream } from 'fs';
+
+initLogger('Debug');
 
 @WebSocketGateway(3100, {
   transports: ['websocket'],
@@ -20,20 +27,194 @@ import { Logger } from '@nestjs/common';
 export class MetaGateway
   implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
   private logger: Logger = new Logger('MetaGateway');
-  // @WebSocketServer() server;
+  private peer: PeerConnection = null;
+  private timer: NodeJS.Timeout;
+  private _webrtcInterval: NodeJS.Timeout;
+  private heartBeatFlag: number;
+  private gameChanel: DataChannel;
+  @WebSocketServer() server: Server;
+
   // @SubscribeMessage('message')
-  // handleMessage(client: any, payload: any): string {
-  //   console.log('payload', payload);
-  //   return 'Hello world!';
+  // handleMessage(client: any, payload: any) {
+  //   this.logger.log(`payload: ${JSON.stringify(payload)}`);
   // }
+
+  @SubscribeMessage('init')
+  handleInit(client: any, payload: any) {
+    this.logger.log(`init: ${JSON.stringify(payload)}`);
+  }
+
+  @SubscribeMessage('heartbeat')
+  handleHeartBeat(client: any, payload: any) {
+    this.logger.log(`heartbeat: ${JSON.stringify(payload)}`);
+    // console.log('hb', payload);
+    this.heartBeatFlag = payload;
+    const pong = {
+      channel_id: '',
+      client_os: '',
+      data: payload,
+      fe_version: '',
+      id: 'heartbeat',
+      packet_id: '',
+      room_id: '',
+      session_id: '',
+      trace_id: '',
+      user_id: '',
+    };
+    if (this.gameChanel.isOpen) {
+      const heartPack = new DataView(new ArrayBuffer(4));
+      heartPack.setUint32(0, 2009889916);
+      this.gameChanel.sendMessageBinary(Buffer.from(heartPack.buffer));
+    }
+    return pong;
+  }
+
+  @SubscribeMessage('init_webrtc')
+  handleInitWebRtc(client: any, payload: any): void {
+    console.log('handleInitWebRtc');
+
+    this.peer = new PeerConnection('roomTest', {
+      iceServers: ['stun:120.24.252.95:3478'],
+    });
+
+    this.peer.onLocalDescription((sdp, type) => {
+      console.warn('peer SDP:', sdp, ' Type:', type);
+      const offer = { sdp, type };
+      const offerFormat = {
+        id: 'offer',
+        data: Buffer.from(JSON.stringify(offer)).toString('base64'),
+      };
+      console.log('send', offerFormat);
+      client.send(JSON.stringify(offerFormat));
+      // return '';
+    });
+
+    this.peer.onLocalCandidate((candidate, mid) => {
+      console.warn('peer Candidate:', candidate);
+      const iceRes = {
+        candidate,
+        sdpMid: mid,
+        sdpMLineIndex: 0,
+      };
+
+      const res = {
+        channel_id: '',
+        client_os: '',
+        data: Buffer.from(JSON.stringify(iceRes)).toString('base64'),
+        fe_version: '',
+        id: 'ice_candidate',
+        packet_id: '',
+        room_id: '',
+        session_id: '',
+        trace_id: '',
+        user_id: '',
+      };
+
+      // client.send(JSON.stringify(res));
+    });
+    this.peer.onStateChange((state) => {
+      console.log('peer-State:', state);
+    });
+    this.peer.onGatheringStateChange((state) => {
+      console.log('GatheringState:', state);
+    });
+    this.gameChanel = this.peer.createDataChannel('start-game');
+
+    this.peer.onDataChannel((dc) => {
+      console.log('onDataChannel', dc);
+    });
+
+    this.gameChanel.onOpen(() => {
+      console.log('channel is open');
+      clearInterval(this.timer);
+      let i = 0;
+      const paths = path.join(__dirname, '../ws/video/earth');
+      console.error('__dirname', __dirname);
+      console.error('paths', paths);
+      if (this.gameChanel.isOpen()) {
+        console.log('gameChanel', this.gameChanel.isOpen());
+        this.sendWertcHeartPack(this.gameChanel);
+      }
+      this.timer = setInterval(() => {
+        if (i < 10) {
+          const steam = createReadStream(paths + `/${i}`);
+          steam.on('data', (data: Buffer) => {
+            this.gameChanel.sendMessageBinary(data);
+          });
+        }
+        i++;
+      }, 2000);
+    });
+    this.gameChanel.onClosed(() => {
+      console.log('gameChanel close');
+      this.stopSendWertcHeartPack();
+    });
+    this.gameChanel.onError(() => {
+      console.log('gameChanel close');
+      this.stopSendWertcHeartPack();
+    });
+  }
+
+  sendWertcHeartPack(channel: DataChannel) {
+    const heartPack = new DataView(new ArrayBuffer(4));
+    heartPack.setUint32(0, 2009889916);
+    this._webrtcInterval = setInterval(() => {
+      channel.sendMessageBinary(Buffer.from(heartPack.buffer));
+    }, 100);
+  }
+
+  stopSendWertcHeartPack(): void {
+    clearInterval(this._webrtcInterval);
+  }
+
+  @SubscribeMessage('ice_candidate')
+  handlerIceCandidate(client: any, payload: any) {
+    const iceCandidate = Buffer.from(payload, 'base64').toString('utf-8');
+    const candidate = JSON.parse(iceCandidate);
+    console.error('收到ice_candidate', candidate);
+    this.peer.addRemoteCandidate(candidate.candidate, candidate.sdpMid);
+  }
+
+  @SubscribeMessage('answer')
+  handerAnswer(client: any, payload: any) {
+    const answer = Buffer.from(payload, 'base64').toString('utf-8');
+    console.log('answer', answer);
+    const clientAnswer = JSON.parse(answer);
+    this.peer.setLocalDescription(clientAnswer.sdp);
+    this.peer.setRemoteDescription(clientAnswer.sdp, clientAnswer.type);
+  }
+
+  @SubscribeMessage('start')
+  handlerWebrtcStart(client: any, payload: any) {
+    console.log('start', payload)
+
+  }
+
   afterInit(server: Server) {
     this.logger.log('Init');
   }
 
   handleConnection(client: WebSocket, ...args: any[]) {
     this.logger.log(`Client connected: ${client.id}`);
+    const connected = {
+      channel_id: '',
+      client_os: '',
+      data: '',
+      fe_version: '',
+      id: 'init',
+      packet_id: '',
+      room_id: '',
+      session_id: '',
+      trace_id: '',
+      user_id: '',
+    };
+    const tt = JSON.stringify(connected);
+    // console.log('tt', tt);
+
+    client.send(tt);
   }
   handleDisconnect(client: WebSocket) {
     this.logger.log(`Client disconnected: ${client.id}`);
+    this.peer && this.peer.close();
   }
 }

+ 186 - 32
src/ws-adapter.ts

@@ -1,50 +1,204 @@
-import * as WebSocket from 'ws';
-import { WebSocketAdapter, INestApplicationContext } from '@nestjs/common';
-import { MessageMappingProperties } from '@nestjs/websockets';
-import { Observable, fromEvent, EMPTY } from 'rxjs';
-import { mergeMap, filter } from 'rxjs/operators';
+import { INestApplicationContext, Logger } from '@nestjs/common';
+import { loadPackage } from '@nestjs/common/utils/load-package.util';
+import { AbstractWsAdapter } from '@nestjs/websockets';
+import {
+  CLOSE_EVENT,
+  CONNECTION_EVENT,
+  ERROR_EVENT,
+} from '@nestjs/websockets/constants';
+import { MessageMappingProperties } from '@nestjs/websockets/gateway-metadata-explorer';
+import * as http from 'http';
+import { EMPTY, fromEvent, Observable } from 'rxjs';
+import { filter, first, mergeMap, share, takeUntil } from 'rxjs/operators';
 
-export class WsAdapter implements WebSocketAdapter {
-  constructor(private app: INestApplicationContext) { }
+let wsPackage: any = {};
 
-  create(port: number, options: any = {}): any {
-    return new WebSocket.Server({ port, ...options });
+enum READY_STATE {
+  CONNECTING_STATE = 0,
+  OPEN_STATE = 1,
+  CLOSING_STATE = 2,
+  CLOSED_STATE = 3,
+}
+
+type HttpServerRegistryKey = number;
+type HttpServerRegistryEntry = any;
+type WsServerRegistryKey = number;
+type WsServerRegistryEntry = any[];
+
+const UNDERLYING_HTTP_SERVER_PORT = 0;
+
+export class WsAdapter extends AbstractWsAdapter {
+  protected readonly logger = new Logger(WsAdapter.name);
+  protected readonly httpServersRegistry = new Map<
+    HttpServerRegistryKey,
+    HttpServerRegistryEntry
+  >();
+  protected readonly wsServersRegistry = new Map<
+    WsServerRegistryKey,
+    WsServerRegistryEntry
+  >();
+
+  constructor(appOrHttpServer?: INestApplicationContext | any) {
+    super(appOrHttpServer);
+    wsPackage = loadPackage('ws', 'WsAdapter', () => require('ws'));
   }
 
-  bindClientConnect(server, callback: Function) {
-    server.on('connection', callback);
+  public create(
+    port: number,
+    options?: Record<string, any> & { namespace?: string; server?: any },
+  ) {
+    const { server, ...wsOptions } = options;
+    if (wsOptions?.namespace) {
+      const error = new Error(
+        '"WsAdapter" does not support namespaces. If you need namespaces in your project, consider using the "@nestjs/platform-socket.io" package instead.',
+      );
+      this.logger.error(error);
+      throw error;
+    }
+
+    if (port === UNDERLYING_HTTP_SERVER_PORT && this.httpServer) {
+      this.ensureHttpServerExists(port, this.httpServer);
+      const wsServer = this.bindErrorHandler(
+        new wsPackage.Server({
+          noServer: true,
+          ...wsOptions,
+        }),
+      );
+
+      this.addWsServerToRegistry(wsServer, port, options.path || '/');
+      return wsServer;
+    }
+
+    if (server) {
+      return server;
+    }
+    if (options.path && port !== UNDERLYING_HTTP_SERVER_PORT) {
+      // Multiple servers with different paths
+      // sharing a single HTTP/S server running on different port
+      // than a regular HTTP application
+      const httpServer = this.ensureHttpServerExists(port);
+      httpServer?.listen(port);
+
+      const wsServer = this.bindErrorHandler(
+        new wsPackage.Server({
+          noServer: true,
+          ...wsOptions,
+        }),
+      );
+      this.addWsServerToRegistry(wsServer, port, options.path);
+      return wsServer;
+    }
+    const wsServer = this.bindErrorHandler(
+      new wsPackage.Server({
+        port,
+        ...wsOptions,
+      }),
+    );
+    return wsServer;
   }
 
-  bindMessageHandlers(
-    client: WebSocket,
+  public bindMessageHandlers(
+    client: any,
     handlers: MessageMappingProperties[],
-    process: (data: any) => Observable<any>,
+    transform: (data: any) => Observable<any>,
   ) {
-    fromEvent(client, 'message')
-      .pipe(
-        mergeMap((data) => this.bindMessageHandler(data, handlers, process)),
-        filter((result) => result),
-      )
-      .subscribe((response) => client.send(JSON.stringify(response)));
+    const close$ = fromEvent(client, CLOSE_EVENT).pipe(share(), first());
+    const source$ = fromEvent(client, 'message').pipe(
+      mergeMap((data) =>
+        this.bindMessageHandler(data, handlers, transform).pipe(
+          filter((result) => result),
+        ),
+      ),
+      takeUntil(close$),
+    );
+    const onMessage = (response: any) => {
+      if (client.readyState !== READY_STATE.OPEN_STATE) {
+        return;
+      }
+      client.send(JSON.stringify(response));
+    };
+    source$.subscribe(onMessage);
   }
 
-  bindMessageHandler(
-    buffer,
+  public bindMessageHandler(
+    buffer: any,
     handlers: MessageMappingProperties[],
-    process: (data: any) => Observable<any>,
+    transform: (data: any) => Observable<any>,
   ): Observable<any> {
-    const message = JSON.parse(buffer.data);
-    const messageHandler = handlers.find(
-      (handler) => handler.message === message.event,
-    );
-    if (!messageHandler) {
+    try {
+      const message = JSON.parse(buffer.data);
+      const messageHandler = handlers.find(
+        (handler) => handler.message === message.id,
+      );
+      const { callback } = messageHandler;
+      return transform(callback(message.data));
+    } catch {
       return EMPTY;
     }
-    console.log('tt', message.data);
-    return process(messageHandler.callback(message.data));
   }
 
-  close(server) {
-    server.close();
+  public bindErrorHandler(server: any) {
+    server.on(CONNECTION_EVENT, (ws: any) =>
+      ws.on(ERROR_EVENT, (err: any) => this.logger.error(err)),
+    );
+    server.on(ERROR_EVENT, (err: any) => this.logger.error(err));
+    return server;
+  }
+
+  public bindClientDisconnect(client: any, callback: Function) {
+    client.on(CLOSE_EVENT, callback);
+  }
+
+  public async dispose() {
+    const closeEventSignals = Array.from(this.httpServersRegistry)
+      .filter(([port]) => port !== UNDERLYING_HTTP_SERVER_PORT)
+      .map(([_, server]) => new Promise((resolve) => server.close(resolve)));
+
+    await Promise.all(closeEventSignals);
+    this.httpServersRegistry.clear();
+    this.wsServersRegistry.clear();
+  }
+
+  protected ensureHttpServerExists(
+    port: number,
+    httpServer = http.createServer(),
+  ) {
+    if (this.httpServersRegistry.has(port)) {
+      return;
+    }
+    this.httpServersRegistry.set(port, httpServer);
+
+    httpServer.on('upgrade', (request, socket, head) => {
+      const baseUrl = 'ws://' + request.headers.host + '/';
+      const pathname = new URL(request.url, baseUrl).pathname;
+      const wsServersCollection = this.wsServersRegistry.get(port);
+
+      let isRequestDelegated = false;
+      for (const wsServer of wsServersCollection) {
+        if (pathname === wsServer.path) {
+          wsServer.handleUpgrade(request, socket, head, (ws: unknown) => {
+            wsServer.emit('connection', ws, request);
+          });
+          isRequestDelegated = true;
+          break;
+        }
+      }
+      if (!isRequestDelegated) {
+        socket.destroy();
+      }
+    });
+    return httpServer;
+  }
+
+  protected addWsServerToRegistry<T extends Record<'path', string> = any>(
+    wsServer: T,
+    port: number,
+    path: string,
+  ) {
+    const entries = this.wsServersRegistry.get(port) ?? [];
+    entries.push(wsServer);
+
+    wsServer.path = path;
+    this.wsServersRegistry.set(port, entries);
   }
 }

+ 27 - 0
ws/HeartBeat.js

@@ -0,0 +1,27 @@
+export default class Heartbeat {
+  constructor(e) {
+    E(this, '_interval', null);
+    E(this, 'ping', () => {
+      const e = Date.now().toString();
+      this.handler.ping(e);
+    });
+    this.handler = e;
+  }
+  ping() {
+    const e = Date.now().toString();
+    this.handler.ping(e);
+  }
+  start() {
+    this.stop(),
+      logger.debug(`Setting ping interval to ${PING_INTERVAL_MS}ms`),
+      (this._interval = window.setInterval(this.ping, PING_INTERVAL_MS));
+  }
+  stop() {
+    logger.debug('stop heartbeat'),
+      this._interval && window.clearInterval(this._interval);
+  }
+  pong(e, t) {
+    !e ||
+      (typeof e == 'string' && this.handler.pong(Date.now() - Number(e), t));
+  }
+}

+ 24 - 0
ws/index.html

@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+
+    <title>test socket</title>
+  </head>
+  <body>
+    <script type="module">
+      const socket = new WebSocket('ws://localhost:3100/ws?id=1111');
+      socket.addEventListener('message', (event) => {
+        console.log('event', event);
+      });
+      const test_message = {
+        data: JSON.stringify({ is_mobile: true }),
+        id: 'init_webrtc',
+      };
+      socket.onopen = (event) => {
+        console.log('onopen', event);
+        socket.send(JSON.stringify(test_message));
+      };
+    </script>
+  </body>
+</html>

+ 0 - 12
ws/test.html

@@ -1,12 +0,0 @@
-<html>
-  <title>test ws</title>
-
-  <body>
-    <script type="module">
-        const 
-
-
-
-    </script>
-  </body>
-</html>

BIN
ws/video/earth/0


BIN
ws/video/earth/1


BIN
ws/video/earth/10


BIN
ws/video/earth/100


BIN
ws/video/earth/101


BIN
ws/video/earth/102


BIN
ws/video/earth/103


BIN
ws/video/earth/104


BIN
ws/video/earth/105


BIN
ws/video/earth/106


BIN
ws/video/earth/107


BIN
ws/video/earth/108


BIN
ws/video/earth/109


BIN
ws/video/earth/11


BIN
ws/video/earth/110


BIN
ws/video/earth/111


BIN
ws/video/earth/112


BIN
ws/video/earth/113


BIN
ws/video/earth/114


BIN
ws/video/earth/115


BIN
ws/video/earth/116


BIN
ws/video/earth/117


BIN
ws/video/earth/118


BIN
ws/video/earth/119


BIN
ws/video/earth/12


BIN
ws/video/earth/120


BIN
ws/video/earth/121


BIN
ws/video/earth/122


BIN
ws/video/earth/123


BIN
ws/video/earth/124


BIN
ws/video/earth/125


BIN
ws/video/earth/126


BIN
ws/video/earth/127


BIN
ws/video/earth/128


BIN
ws/video/earth/129


BIN
ws/video/earth/13


BIN
ws/video/earth/130


BIN
ws/video/earth/131


BIN
ws/video/earth/132


BIN
ws/video/earth/133


BIN
ws/video/earth/134


BIN
ws/video/earth/135


BIN
ws/video/earth/136


BIN
ws/video/earth/137


BIN
ws/video/earth/138


BIN
ws/video/earth/139


BIN
ws/video/earth/14


BIN
ws/video/earth/140


BIN
ws/video/earth/141


BIN
ws/video/earth/142


BIN
ws/video/earth/143


BIN
ws/video/earth/144


BIN
ws/video/earth/145


BIN
ws/video/earth/146


BIN
ws/video/earth/147


BIN
ws/video/earth/148


BIN
ws/video/earth/149


BIN
ws/video/earth/15


BIN
ws/video/earth/150


BIN
ws/video/earth/151


BIN
ws/video/earth/152


BIN
ws/video/earth/153


BIN
ws/video/earth/154


BIN
ws/video/earth/155


BIN
ws/video/earth/156


BIN
ws/video/earth/157


BIN
ws/video/earth/158


BIN
ws/video/earth/159


BIN
ws/video/earth/16


BIN
ws/video/earth/160


BIN
ws/video/earth/161


BIN
ws/video/earth/162


BIN
ws/video/earth/163


BIN
ws/video/earth/164


BIN
ws/video/earth/165


BIN
ws/video/earth/166


BIN
ws/video/earth/167


BIN
ws/video/earth/168


BIN
ws/video/earth/169


BIN
ws/video/earth/17


BIN
ws/video/earth/170


BIN
ws/video/earth/171


BIN
ws/video/earth/172


BIN
ws/video/earth/173


BIN
ws/video/earth/174


BIN
ws/video/earth/175


BIN
ws/video/earth/176


BIN
ws/video/earth/177


BIN
ws/video/earth/178


BIN
ws/video/earth/179


+ 0 - 0
ws/video/earth/18


Некоторые файлы не были показаны из-за большого количества измененных файлов