แแ แกแขแแขแแแจแ แแกแฃแ แก แแแแแแแแ แแ แฉแแแ แแชแแแแแแแแ แแแแแแก แกแขแ แแแแแแ แแแแกแแแแขแแแแก แกแแจแฃแแแแแแ แแแกแแแ แแฎแแ แแก แแ แแฃแแแ แแก แแแแแแแขแแแแก แแแแแงแแแแแแก แแแ แแจแ, แ แแแแ แแชแแ Adobe Flash Player. แฌแแแแแแฎแแ, แ แแ แแแแแแ, แ แ แแแแแแแแ.
Adobe Flash, แแแ แ Macromedia Flash, แแ แแก แแแแขแคแแ แแ แแแแแแแชแแแแแก แจแแกแแฅแแแแแแ, แ แแแแแแแช แแฃแจแแแแแ แแแ แแ แแฃแแแ แจแ. Media Stream API-แแก แแแแแ แแแแแแ แแก แแ แแฅแขแแแฃแแแ แแ แแแแแ แแ แแแแขแคแแ แแ แแงแ แแแแแแแแ แแแแ แแแแแแกแ แแ แฎแแแก แแแแแกแแชแแแแ, แแกแแแ แแ แแฃแแแ แจแ แกแฎแแแแแกแฎแแ แกแแฎแแก แแแแคแแ แแแชแแแแแกแ แแ แฉแแขแแแแก แจแแกแแฅแแแแแแ. แแแแแ แแแคแแ แแแชแแแก แแแแแชแแแแก แแ แแขแแแแแ RTMP (Real Time Messaging Protocol) แคแแฅแขแแแ แแแแ แแแแ แฎแแแ แแแแฎแฃแ แ, แ แแช แแแจแแแแก: แแฃ แแกแฃแ แ แแฅแแแแ แแแแแแแก แกแแ แแแกแแก แแแซแแแแ แแแ, แแงแแแแ แกแแแแแ แแกแแ แแแแแแ, แแแแแแงแแแแ แแแแแ Adobe-แก แแ แแแ แแแฃแแ แฃแแ แฃแแแแแงแแคแ - Adobe Media Server (AMS).
แแแ แแแแฃแแ แแแ แแแแแก แจแแแแแ, 2012 แฌแแแก, Adobe-แแ โแแแแขแแแ แแ แแแแคแฃแ แแฎแโ แกแแแแแแแแแแแก.
Adobe Flash แแแแขแคแแ แแ 20 แฌแแแแ แแแขแ แฎแแแกแแ, แแ แแ แแแก แแแแแแแแแแแจแ แแฆแแแฉแแแแแแ แแ แแแแแ แแ แแขแแแฃแแ แแแฃแชแแแแแแ, แแฎแแ แแแญแแ แ
แฉแแแ แแ แแแฅแขแแกแแแแก, แแ แแแจแแแแ แแแแแแฌแงแแแขแ แแแแแแแแ แแแแแขแแแแแแแ Flash-แแก แแแแแงแแแแแ แแ แแฃแแแ แจแ. แแ แแแแแ แแฆแแแแจแแ แแแแแแ แ แแแแแแ; Flash แแกแแแ แกแแแ แแแ แแ แแ แแก แแฎแแ แแแญแแ แแแ แแแแแแฃแ แแแแขแคแแ แแแแแ แแ แแ แแแแแแแแแ แแ แแแแแแแ Adobe Flash-แแก แแแแแแแกแแแ Windows-แแ (แฆแแแแแก แแแฃแแแขแแ แ) แแแแแแแแ แแแแกแแแแก. แแแแขแแ แแแแแฌแงแ แแแแแแขแแก แแแฌแแ แ JavaScript-แจแ. แแก แแฅแแแแ แแฎแแแแ แแ แแขแแขแแแ, แ แแแแแ แแแแแแแแแแแ แแแแแแ, แ แแ แกแขแ แแแแแแแก แแแแแแแแ แแแแ แแ แฃแคแ แ แแคแแฅแขแฃแ แแ แจแแแซแแแแ p2p-แแ แแแงแ แแแแแแ, แแฎแแแแ แฉแแแแแแก แแก แแฅแแแแ peer - server - peers, แแแแ แแ แฃแคแ แ แแแแก แจแแกแแฎแแ แกแฎแแ แแ แแก, แ แแแแแ แแก แฏแแ แแ แแ แแก แแแแ.
แแแกแแฌแงแแแแ, แฉแแแ แแแญแแ แแแแ แ แแแแฃแ แ websockets แกแแ แแแ แ. แแ แจแแแฅแแแแ แฃแแแ แขแแแแกแ แแแแแแแ go แแแแแขแแก แกแแคแฃแซแแแแแ:
แกแแ แแแ แแก แแแแ
package main
import (
"errors"
"github.com/go-chi/chi"
"gopkg.in/olahol/melody.v1"
"log"
"net/http"
"time"
)
func main() {
r := chi.NewRouter()
m := melody.New()
m.Config.MaxMessageSize = 204800
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/index.html")
})
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
m.HandleRequest(w, r)
})
// ะัะพะดะบะฐััะธะผ ะฒะธะดะตะพ ะฟะพัะพะบ
m.HandleMessageBinary(func(s *melody.Session, msg []byte) {
m.BroadcastBinary(msg)
})
log.Println("Starting server...")
http.ListenAndServe(":3000", r)
}
แแแแแแขแแ (แกแขแ แแแแแแแก แแฎแแ แแก), แฏแแ แแแแแ แแแ แฌแแแแแ แแญแแ แแแแแ. แแก แแแแแแแ แแแจแแแแแแ
แฉแแแ แแแฆแแแ แฌแแแแแแก (แแแแแ แแแแก) แแแแแ แแแ/แแแแ แแคแแแแ แแแจแแแแแแ
แจแแแแแแ, getUserMedia() แแแ แฃแแแแก Promise-แก, แ แแแแแกแแช แแแแแกแชแแแก MediaStream แแแแแฅแขแก - แแแแแ-แแฃแแแ แแแแแชแแแแแแก แแแแแแก. แฉแแแ แแ แแแแแฅแขแก แแแแแญแแแ แแแแแ แแแแแแแขแแก src แแแแกแแแแก. แแแแ:
แกแแแแฃแฌแงแแแแ แแฎแแ แ
<style>
#videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- ะะดะตัั ะฒ ััะพะผ "ะพะบะพัะตัะบะต" ะบะปะธะตะฝั ะฑัะดะตั ะฒะธะดะตัั ัะตะฑั -->
<video autoplay id="videoObjectHtml5ApiServer"></video>
<script type="application/javascript">
var
video = document.getElementById('videoObjectHtml5ApiServer');
// ะตัะปะธ ะดะพัััะฟะตะฝ MediaDevices API, ะฟััะฐะตะผัั ะฟะพะปััะธัั ะดะพัััะฟ ะบ ะบะฐะผะตัะต (ะผะพะถะฝะพ ะตัะต ะธ ะบ ะผะธะบัะพัะพะฝั)
// getUserMedia ะฒะตัะฝะตั ะพะฑะตัะฐะฝะธะต, ะฝะฐ ะบะพัะพัะพะต ะฟะพะดะฟะธััะฒะฐะตะผัั ะธ ะฟะพะปััะตะฝะฝัะน ะฒะธะดะตะพะฟะพัะพะบ ะฒ ะบะพะปะฑะตะบะต ะฝะฐะฟัะฐะฒะปัะตะผ ะฒ video ะพะฑัะตะบั ะฝะฐ ัััะฐะฝะธัะต
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
// ะฒะธะดะตะพ ะฟะพัะพะบ ะฟัะธะฒัะทัะฒะฐะตะผ ะบ video ัะตะณั, ััะพะฑั ะบะปะธะตะฝั ะผะพะณ ะฒะธะดะตัั ัะตะฑั ะธ ะบะพะฝััะพะปะธัะพะฒะฐัั
video.srcObject = stream;
});
}
</script>
แกแแแแขแแแแก แกแแจแฃแแแแแแ แแแแแ แแแแแแแก แแแแแกแแชแแแแ, แกแแญแแ แแ แกแแแแ แแแจแแคแแ แ, แแฃแคแแ แฃแแ แแ แแแฌแแแแแแ แแแแแกแชแแก. แแแแแ แแแแแ แแแแแแแก แแแแแชแแแ แจแแฃแซแแแแแแแ แแแแกแแแแขแแแแก แกแแจแฃแแแแแแ. แกแฌแแ แแ แแฅ แแ แแก แกแแฅแแ แฉแแแแก แแแฎแแแ แแแแจแ
แฉแแแ แแแจแแคแ แแแ แแแแแ แแแแแแก, แแงแแคแ แแแก แแแฌแแแแแแ
<style>
#videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- ะะดะตัั ะฒ ััะพะผ "ะพะบะพัะตัะบะต" ะบะปะธะตะฝั ะฑัะดะตั ะฒะธะดะตัั ัะตะฑั -->
<video autoplay id="videoObjectHtml5ApiServer"></video>
<script type="application/javascript">
var
video = document.getElementById('videoObjectHtml5ApiServer');
// ะตัะปะธ ะดะพัััะฟะตะฝ MediaDevices API, ะฟััะฐะตะผัั ะฟะพะปััะธัั ะดะพัััะฟ ะบ ะบะฐะผะตัะต (ะผะพะถะฝะพ ะตัะต ะธ ะบ ะผะธะบัะพัะพะฝั)
// getUserMedia ะฒะตัะฝะตั ะพะฑะตัะฐะฝะธะต, ะฝะฐ ะบะพัะพัะพะต ะฟะพะดะฟะธััะฒะฐะตะผัั ะธ ะฟะพะปััะตะฝะฝัะน ะฒะธะดะตะพะฟะพัะพะบ ะฒ ะบะพะปะฑะตะบะต ะฝะฐะฟัะฐะฒะปัะตะผ ะฒ video ะพะฑัะตะบั ะฝะฐ ัััะฐะฝะธัะต
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
// ะฒะธะดะตะพ ะฟะพัะพะบ ะฟัะธะฒัะทัะฒะฐะตะผ ะบ video ัะตะณั, ััะพะฑั ะบะปะธะตะฝั ะผะพะณ ะฒะธะดะตัั ัะตะฑั ะธ ะบะพะฝััะพะปะธัะพะฒะฐัั
video.srcObject = s;
var
recorderOptions = {
mimeType: 'video/webm; codecs=vp8' // ะฑัะดะตะผ ะบะพะดะธัะพะฒะฐัั ะฒะธะดะตะพะฟะพัะพะบ ะฒ ัะพัะผะฐั webm ะบะพะดะตะบะพะผ vp8
},
mediaRecorder = new MediaRecorder(s, recorderOptions ); // ะพะฑัะตะบั MediaRecorder
mediaRecorder.ondataavailable = function(e) {
if (e.data && e.data.size > 0) {
// ะฟะพะปััะฐะตะผ ะบััะพัะตะบ ะฒะธะดะตะพะฟะพัะพะบะฐ ะฒ e.data
}
}
mediaRecorder.start(100); // ะดะตะปะธั ะฟะพัะพะบ ะฝะฐ ะบััะพัะบะธ ะฟะพ 100 ะผั ะบะฐะถะดัะน
});
}
</script>
แแฎแแ แแแแแ แแแแแแแขแแ แแแแแชแแแ แแแแกแแแแขแแแแก แกแแจแฃแแแแแแ. แแแกแแแแแ แแ, แ แแ แแแแกแแแแแก แแฎแแแแ แแแแแฅแขแแ แกแแญแแ แ
แฉแแแ แแแแแแชแแแ แแแแแ แแแแแแก แกแแ แแแ แแ
<style>
#videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- ะะดะตัั ะฒ ััะพะผ "ะพะบะพัะตัะบะต" ะบะปะธะตะฝั ะฑัะดะตั ะฒะธะดะตัั ัะตะฑั -->
<video autoplay id="videoObjectHtml5ApiServer"></video>
<script type="application/javascript">
var
video = document.getElementById('videoObjectHtml5ApiServer');
// ะตัะปะธ ะดะพัััะฟะตะฝ MediaDevices API, ะฟััะฐะตะผัั ะฟะพะปััะธัั ะดะพัััะฟ ะบ ะบะฐะผะตัะต (ะผะพะถะฝะพ ะตัะต ะธ ะบ ะผะธะบัะพัะพะฝั)
// getUserMedia ะฒะตัะฝะตั ะพะฑะตัะฐะฝะธะต, ะฝะฐ ะบะพัะพัะพะต ะฟะพะดะฟะธััะฒะฐะตะผัั ะธ ะฟะพะปััะตะฝะฝัะน ะฒะธะดะตะพะฟะพัะพะบ ะฒ ะบะพะปะฑะตะบะต ะฝะฐะฟัะฐะฒะปัะตะผ ะฒ video ะพะฑัะตะบั ะฝะฐ ัััะฐะฝะธัะต
if (navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia({video: true}).then(function (stream) {
// ะฒะธะดะตะพ ะฟะพัะพะบ ะฟัะธะฒัะทัะฒะฐะตะผ ะบ video ัะตะณั, ััะพะฑั ะบะปะธะตะฝั ะผะพะณ ะฒะธะดะตัั ัะตะฑั ะธ ะบะพะฝััะพะปะธัะพะฒะฐัั
video.srcObject = s;
var
recorderOptions = {
mimeType: 'video/webm; codecs=vp8' // ะฑัะดะตะผ ะบะพะดะธัะพะฒะฐัั ะฒะธะดะตะพะฟะพัะพะบ ะฒ ัะพัะผะฐั webm ะบะพะดะตะบะพะผ vp8
},
mediaRecorder = new MediaRecorder(s, recorderOptions ), // ะพะฑัะตะบั MediaRecorder
socket = new WebSocket('ws://127.0.0.1:3000/ws');
mediaRecorder.ondataavailable = function(e) {
if (e.data && e.data.size > 0) {
// ะฟะพะปััะฐะตะผ ะบััะพัะตะบ ะฒะธะดะตะพะฟะพัะพะบะฐ ะฒ e.data
socket.send(e.data);
}
}
mediaRecorder.start(100); // ะดะตะปะธั ะฟะพัะพะบ ะฝะฐ ะบััะพัะบะธ ะฟะพ 100 ะผั ะบะฐะถะดัะน
}).catch(function (err) { console.log(err); });
}
</script>
แกแแแแฃแฌแงแแแแ แแฎแแ แ แแแแ แแ แแก! แแฎแแ แแชแแแแ แแแแแ แแแแแแแก แแแฆแแแ แแ แแแแแแขแแ แฉแแแแแแ. แ แ แแแญแแ แแแแ แแแแกแแแแก? แแแ แแแ แ แแแจแ, แ แ แแฅแแ แฃแแแ, แกแแแแขแแก แแแแจแแ แ. แฉแแแ แแแแแแ แแแ โแแกแแแแแแกโ WebSocket แแแแแฅแขแก แแ แแแแแแฌแแ แ โแจแแขแงแแแแแแแแแแกโ แฆแแแแกแซแแแแแก. แแ แแแแแ แแแแแชแแแแ แแแฌแแแแก แแแฆแแแแก แจแแแแแ, แฉแแแแ แกแแ แแแ แ แแแก แแแแแกแชแแแก แแแแแแแขแแแก, แแแฃ แแแแแแขแแแก. แแ แจแแแแฎแแแแแจแ, โแจแแขแงแแแแแแแแแแกโ แแแแแแแแก โแแกแแแแแแแแโ แแแแแแจแแ แแแฃแแ แแแแแซแแฎแแแแก แคแฃแแฅแชแแ แแแแฅแแแแแแแ แแแแแแขแแ; แแแแแ แแแแแฅแขแ แแแแแแแก แคแฃแแฅแชแแแก แแ แแฃแแแแขแจแ - vp8-แแ แแแจแแคแ แฃแแ แแแแแ แแแแแแแก แแแฌแแแ.
แฉแแแ แแแฆแแแ แแแแแ แแแแแแก
<style>
#videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- ะะดะตัั ะฒ ััะพะผ "ะพะบะพัะตัะบะต" ะบะปะธะตะฝั ะฑัะดะตั ะฒะธะดะตัั ัะตะฑั -->
<video autoplay id="videoObjectHtml5ApiServer"></video>
<script type="application/javascript">
var
video = document.getElementById('videoObjectHtml5ApiServer'),
socket = new WebSocket('ws://127.0.0.1:3000/ws'),
arrayOfBlobs = [];
socket.addEventListener('message', function (event) {
// "ะบะปะฐะดะตะผ" ะฟะพะปััะตะฝะฝัะน ะบััะพัะตะบ ะฒ ะผะฐััะธะฒ
arrayOfBlobs.push(event.data);
// ะทะดะตัั ะฑัะดะตะผ ัะธัะฐัั ะบััะพัะบะธ
readChunk();
});
</script>
แแแแ แฎแแแก แแแแแแแแแแแจแ แแชแแแแแแแ แแแแแแ, แ แแขแแ แแ แแก แจแแฃแซแแแแแแแ แแแฆแแแฃแแ แแแฌแแแแแแก แแแฃแงแแแแแแแ แแแแแแแแ แแแแแ แแแแแแแขแแ แแแกแแแ แแแแ, แแแแ แแ แแฆแแแฉแแแ, แ แแ แแแแก แแแแแแแแ แจแแฃแซแแแแแแแ, แ แ แแฅแแ แฃแแแ, แฏแแ แฃแแแ แฉแแกแแแ แแแญแแ แ แกแแแชแแแแฃแ แแฃแคแแ แจแ, แ แแแแแแช แจแแแ แฃแแแ แแแแแ แแแแแแแขแก แแ แแฎแแแแ แแแแก แจแแแแแ แแแแฌแงแแแก แแแแแ แแแแแแแก แแแแแ แแก. แแแแกแแแแก แแแแญแแ แแแแแ
MediaSource แแแฅแแแแแแก แ แแแแ แช แแ แแแแแ แ แจแฃแแแแแแแ แแแแแแก แแแแแ แแก แแแแแฅแขแกแ แแ แแ แแแแแ แแแแแแแก แฌแงแแ แแก แจแแ แแก. MediaSource แแแแแฅแขแ แจแแแชแแแก แฉแแ แแแแก แแฃแคแแ แก แแแแแ/แแฃแแแ แแแแแแแก แฌแงแแ แแกแแแแก. แแ แแ แแแฎแแกแแแแแแแแแ แแก, แ แแ แแฃแคแแ แก แจแแฃแซแแแ แแฎแแแแ Uint8 แแแแแชแแแแแแก แจแแแแฎแแ, แแกแ แ แแ แแฅแแแ แแแแญแแ แแแแแ FileReader แแกแแแ แแฃแคแแ แแก แจแแกแแฅแแแแแแ. แแแแแฎแแแ แแแแก แแ แฃแคแ แ แแแแแแ แแแฎแแแแ:
แแแแแแแแ แแแแก แแแแแ แแแแแแแก แแแแแ แ
<style>
#videoObjectHtml5ApiServer { width: 320px; height: 240px; background: #666; }
</style>
</head>
<body>
<!-- ะะดะตัั ะฒ ััะพะผ "ะพะบะพัะตัะบะต" ะบะปะธะตะฝั ะฑัะดะตั ะฒะธะดะตัั ัะตะฑั -->
<video autoplay id="videoObjectHtml5ApiServer"></video>
<script type="application/javascript">
var
video = document.getElementById('videoObjectHtml5ApiServer'),
socket = new WebSocket('ws://127.0.0.1:3000/ws'),
mediaSource = new MediaSource(), // ะพะฑัะตะบั MediaSource
vid2url = URL.createObjectURL(mediaSource), // ัะพะทะดะฐะตะผ ะพะฑัะตะบั URL ะดะปั ัะฒัะทัะฒะฐะฝะธั ะฒะธะดะตะพะฟะพัะพะบะฐ ั ะฟัะพะธะณััะฒะฐัะตะปะตะผ
arrayOfBlobs = [],
sourceBuffer = null; // ะฑััะตั, ะฟะพะบะฐ ะฝัะปั-ะพะฑัะตะบั
socket.addEventListener('message', function (event) {
// "ะบะปะฐะดะตะผ" ะฟะพะปััะตะฝะฝัะน ะบััะพัะตะบ ะฒ ะผะฐััะธะฒ
arrayOfBlobs.push(event.data);
// ะทะดะตัั ะฑัะดะตะผ ัะธัะฐัั ะบััะพัะบะธ
readChunk();
});
// ะบะฐะบ ัะพะปัะบะพ MediaSource ะฑัะดะตั ะพะฟะพะฒะตัะตะฝ , ััะพ ะธััะพัะฝะธะบ ะณะพัะพะฒ ะพัะดะฐะฒะฐัั ะบััะพัะบะธ
// ะฒะธะดะตะพ/ะฐัะดะธะพ ะฟะพัะพะบะฐ
// ัะพะทะดะฐะตะผ ะฑััะตั , ัะปะตะดัะตั ะพะฑัะฐัะธัั ะฒะฝะธะผะฐะฝะธะต, ััะพ ะฑััะตั ะดะพะปะถะตะฝ ะทะฝะฐัั ะฒ ะบะฐะบะพะผ ัะพัะผะฐัะต
// ะบะฐะบะธะผ ะบะพะดะตะบะพะผ ะฑัะป ะทะฐะบะพะดะธัะพะฒะฐะฝ ะฟะพัะพะบ, ััะพะฑั ัะตะผ ะถะต ัะฟะพัะพะฑะพะผ ะฟัะพัะธัะฐัั ะฒะธะดะตะพะฟะพัะพะบ
mediaSource.addEventListener('sourceopen', function() {
var mediaSource = this;
sourceBuffer = mediaSource.addSourceBuffer("video/webm; codecs="vp8"");
});
function readChunk() {
var reader = new FileReader();
reader.onload = function(e) {
// ะบะฐะบ ัะพะปัะบะพ FileReader ะฑัะดะตั ะณะพัะพะฒ, ะธ ะทะฐะณััะทะธั ัะตะฑะต ะบััะพัะตะบ ะฒะธะดะตะพะฟะพัะพะบะฐ
// ะผั "ะฟัะธัะตะฟะปัะตะผ" ะฟะตัะตะบะพะดะธัะพะฒะฐะฝะฝัะน ะฒ Uint8Array (ะฑัะป Blob) ะบััะพัะตะบ ะฒ ะฑััะตั, ัะฒัะทะฐะฝะฝัะน
// ั ะฟัะพะธะณััะฒะฐัะตะปะตะผ, ะธ ะฟัะพะธะณััะฒะฐัะตะปั ะฝะฐัะธะฝะฐะตั ะฒะพัะฟัะพะธะทะฒะพะดะธัั ะฟะพะปััะตะฝะฝัะน ะบััะพัะตะบ ะฒะธะดะตะพ/ะฐัะดะธะพ
sourceBuffer.appendBuffer(new Uint8Array(e.target.result));
reader.onload = null;
}
reader.readAsArrayBuffer(arrayOfBlobs.shift());
}
</script>
แกแขแ แแแแแแแก แกแแ แแแกแแก แแ แแขแแขแแแ แแแแ แแ แแก. แแแแแแ แ แแแแฃแกแ แแ แแก แแก, แ แแ แแแแแแก แแแแแ แ แฉแแแแ แฉแแแ แแแแแแชแแ แแฎแแ แแก 100 ms-แแ; แฉแแแ แแแแแแ แแแงแแแแแ แแแแก, แ แแแแกแแช แแงแแคแ แแแแแ แแแแแแก แกแแ แแแ แแ แแแแแชแแแแแแ. แฃแคแ แ แแแขแแช, แ แแแแกแแช แฉแแแก แแแแขแแแก แจแแแแแแฌแแ, แแแแแแชแแ แแ แแแแฆแแ แแฎแแ แแแแก แจแแ แแก แฉแแแแ แฉแแแ แแแแแแแแ แแแแ แแแแ, แแก แแจแแแ แแ แฉแแแแ. แแแแแฌแงแ แแ แแแแฃแกแแก แแแซแแแแแก แแแแแแก แซแแแแ แแ... แจแแแฎแแแ
แฌแงแแ แ: www.habr.com