Pull to refresh

Массовая запись с камер на выборах — 2

Reading time 7 min
Views 26K
Хабр — не для политики. В данной статье рассматриваются исключительно технические аспекты реализации конкретного программного решения. Для всеобщего блага просьба отказаться от каких-либо политических дебатов, выступлений, агитации и тому подобных действий в комментариях. Кроме того, просьба не применять полученные знания в деструктивных целях, не начинать бекапить весь видеоархив без особой надобности и так далее. Спасибо.

8 сентября, единый день голосования. В этом году широкой общественности предлагается наблюдать через интернет за выборами столичного мэра. Ряд граждан считают для себя интересным записать картинку с камер: у кого-то политически мотивированный интерес, а большинству просто банально любопытно посмотреть на себя и знакомых глазами Интернета. Данная статья призвана продемонстировать принципы работы текущей системы и предложить работающие концепты.


Со времен прошлых выборов система немножечко изменилась (иначе и статьи бы не было), поэтому сначала вспомним, как всё работало раньше и как стало работать сейчас. Итак, у каждой камеры есть уникальный uid и пул серверов, с которых сыпется видео. Сформировав с использованием этих данных особый запрос, можно получить ссылку на кусочек видео, записанного выбранной камерой.

Для начала найдем данные обо всех существующих камерах. Мне показался наиболее простым следующий способ: начнем поиск по номеру участка, с 1 до 3800. Для этого отправим GET vybory.mos.ru/json/id_searchaaa/bbb.json, где bbb это uid, а aaa это len(bbb). Например, vybory.mos.ru/json/id_search/1/3.json

Получим json с информацией об этом участке, что-то вроде вот этого:
[{"id":7933,"name":"Участок избирательной комиссии №3","num":"3","location_id":1162,"address":"Новый Арбат, 36/9","raw_address":"г.Москва, Новый Арбат ул., дом 36/9","is_standalone":false,"size":null,"location":{"id":1162,"address":"Россия, Москва, улица Новый Арбат, 36/9","raw_address":"г.Москва, Новый Арбат ул., дом 36/9","district_id":1,"area_id":null,"sub_area_id":null,"locality_id":1,"street_id":1590,"lat":55.753266,"lon":37.577301,"max_zoom":17}}]


Особый интерес здесь представляет id. Отправим GET вида vybory.mos.ru/account/channels?station_id=id, в данном случае vybory.mos.ru/account/channels?station_id=7933

В ответе получим строчку с кракозяблами, на которые ругается мой редактор, но содержащие внутри хеши камер и адреса серверов. Выдерем оттуда хеши регуляркой вида
\$([0-9a-h]{8}-[0-9a-h]{4}-[0-9a-h]{4}-[0-9a-h]{4}-[0-9a-h]{12}) и ip адреса регуляркой вида .*?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})

В результате получим требуемую информацию о камерах текущего участка:
2e9dd8dc-edd4-11e2-9a6b-f0def1c0f84c 188.254.112.2 188.254.112.3 188.254.112.4
2ea32990-edd4-11e2-9a6b-f0def1c0f84c 188.254.112.2 188.254.112.3 188.254.112.4

Далее начинаются ньюансы. Существует три типа камер: старые, новые и отсутствующие. Чем они отличаются я расскажу чуть позже, сначала разберемся, как их различать, а различать их очень просто — нужно отправить GET вида http://SERVER/master.m3u8?cid=UID
Новая камера вернет нечто вроде

#EXTM3U
#EXT-X-VERSION:2
#EXT-X-STREAM-INF:PROGRAM-ID=777,BANDWIDTH=3145728
/variant.m3u8?cid=e1164950-0c19-11e3-803b-00163ebf8df9&var=orig


Старая камера вернет что-то такого вида:
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:136
#EXT-X-TARGETDURATION:15
#EXT-X-ALLOW-CACHE:NO
#EXT-X-PROGRAM-DATE-TIME:2013-09-04T12:05:40Z
#EXTINF:15,
/segment.ts?cid=2ea32990-edd4-11e2-9a6b-f0def1c0f84c&var=orig&ts=1378296340.93-1378296355.93
#EXTINF:15,
/segment.ts?cid=2ea32990-edd4-11e2-9a6b-f0def1c0f84c&var=orig&ts=1378296355.93-1378296370.93
#EXTINF:15,
/segment.ts?cid=2ea32990-edd4-11e2-9a6b-f0def1c0f84c&var=orig&ts=1378296370.93-1378296385.93
#EXTINF:15,
/segment.ts?cid=2ea32990-edd4-11e2-9a6b-f0def1c0f84c&var=orig&ts=1378296385.93-1378296400.93


Отсутствующая камера не вернет ничего, кроме 404 CID Was Not Found :)

Теперь, когда мы умеем получать информацию о камерах конкретного участка, напишем многопоточную парсилку, которая соберет нам всю необходимую информацию. Я предпочитаю складывать данные в бесплатный монголаб, но вполне можно обойтись и обычным shelve. Зная, что участков в москве 3500+, пробежимся циклом от 1 до 3800. Ниже набросанный на коленке, но тем не менее, работающий код. В нем, разумеется, нужно вписать своё печенько и пароли от сервера монги.

# -*- coding: utf-8 -*-
import json, re
import httplib
import threading
from time import sleep
import Queue
from pymongo import MongoClient

client = MongoClient('mongodb://admin:кусь@кусь.mongolab.com:43368/elections')

db = client['elections']
data = db['data']

data.drop()

def get_data(uid):
    print uid
    headers = {'Origin': 'vybory.mos.ru',
    'X-Requested-With': 'XMLHttpRequest',
    'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0);',
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
    'Accept': '*/*',
    'Referer': 'http://vybory.mos.ru/',
    'Accept-Encoding': 'deflate,sdch',
    'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4',
    'Accept-Charset': 'windows-1251,utf-8;q=0.7,*;q=0.3',
    'Cookie': 'rack.session=кусь'
    }

    try:
        conn = httplib.HTTPConnection('vybory.mos.ru')
        conn.request('GET', '/json/id_search/%d/%d.json'%(len(str(uid)), uid), None,headers)
        resp = conn.getresponse()
        try:
            content = json.loads(resp.read())[0]
            conn.request('GET', '/account/channels?station_id=%s'%content['id'], None,headers)
            resp = conn.getresponse()
            cont = resp.read()

            cnt=0
            for i in cont.split('\x00')[1:]:
                cnt+=1
                uid=re.findall(r'\$([0-9a-h]{8}-[0-9a-h]{4}-[0-9a-h]{4}-[0-9a-h]{4}-[0-9a-h]{12})', i)[0]
                ip=re.findall(r'.*?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', i)

                conn2 = httplib.HTTPConnection('%s'%ip[0])
                conn2.request('GET', '/master.m3u8?cid=%s'%(uid), None,headers)
                info = conn2.getresponse().read()
                conn2.close()
                if '/segment.ts' in info:
                    camtype='old'
                elif '/variant.m3u8' in info:
                    camtype='new'
                else:
                    camtype='nil'

                #print content
                data.insert({
                            'name':content['name'],
                            'num':content['num'],
                            'addr':content['address'],
                            'uid':uid,
                            'ip':ip,
                            'cnt':str(cnt),
                            'type':camtype
                            })

        except Exception,e:
            pass

    except Exception,e:
        print e
    conn.close()


queue = Queue.Queue()
def repeat():
    while True:
        try:
            item = queue.get_nowait()
        except Queue.Empty:
            break
        get_data(item)
        sleep(0.01)
        queue.task_done()

for i in xrange(1, 3800):
    queue.put(i)

for i in xrange(10):
    t = threading.Thread(target=repeat)
    t.start()
queue.join()

print data.find().count(),'all cams'
print data.find({'type':'nil'}).count(),'offline cams'
print data.find({'type':'old'}).count(),'old cams'
print data.find({'type':'new'}).count(),'new cams'


Теперь у нас есть полностью собранная база камер. На момент написания статьи старых камер было 544, с ними, увы, получится работать только по-старому.
Но теперь у нас есть и 5778 новых камер, и у них есть одна особенность. Чанки со старых камер спустя очень короткое время протухают — нужно постоянно скачивать свежий плейлист, выдирать оттуда линки на чанки и качать их, пока не протухли. Новые камеры лишены этого недостатка. Можно качать чанки произвольных размеров за произвольный период времени, отправив GET вида http://SERVER/segment.ts?cid=UID&var=orig&ts=BEGIN-END Между BEGIN и END может быть не 15 секунд, а гораздо больше. Я остановился на чанках длительностью 5 минут. На самом деле, можно указать хоть час, но в некоторых случаях, насколько я могу судить, если трансляция прерывалась в течение пределов чанка, не скачается весь чанк. Грубо говоря, если вы пытаетесь скачать 8 часов из архива чанками по часу и при этом в течение нескольких минут одного чанка трансляции фактически не было, не скачается весь часовой чанк. Поэтому разумно выбрать чанк поменьше. Гуру алгоритмизации (которых, как мы помним, 10%) могут написать свой бинарный поиск, дабы не пропало ни секунды видео =)
Кстати, дабы закрыть вопрос — отсутствующей называется камера, которая зарегистрирована в портале, но по факту не работает.

Автоматизируем процесс скачивания. Здесь можно было сгородить свой многопоточный велосипед на питоне, но я решил воспользоваться сторонним софтом. Мы будем генерить метафайл со ссылками на чанки для aria2c, метафайлы для tsmuxer и последовательно их запускать.

Например, вот как-то так:
# -*- coding: utf-8 -*-
from time import sleep, time
from pymongo import MongoClient
import os
import subprocess
import shutil


#Корневая папка, куда будем складировать чанки
directory='e:/dumps'
#Размер чанка
delta=300
#Номер избирательного участка
num='666'


client = MongoClient('mongodb://кусь:кусь@кусь.mongolab.com:43368/elections')
db = client['elections']
data = db['data']


#Качать видео за последние 8 часов
start=int(time())-3600*8

#Создаем папку для дампов с избирательного участка
try:
    os.mkdir('%s/%s'%(directory,num))
except:
    pass

#Лезем в базу и достаем оттуда информацию о камерах с участка
for i in data.find({'num':num}):
    if i['type']=='nil':
        print 'Offline camera',i['uid']
    elif i['type']=='old':
        print 'Old camera',i['uid']
    else:
        print 'New camera',i['uid']
        f=open('links-%s-%s.txt'%(num, i['cnt']),'w')
        #Создаем поддиректории для каждой камеры
        try:
            os.mkdir('%s/%s/%s'%(directory,num,i['cnt']))
        except:
            pass

        cur=start
        files=''

        #Генерируем ссылки на чанки выбранной длины
        while True:
            if cur+delta>time():
                for ip in i['ip']:
                    url = 'http://{0}/segment.ts?cid={1}&var=orig&ts={2}.00-{3}'.format(ip,
                                                                                   i['uid'],
                                                                                   cur, time())
                    f.write('%s\t'%url)
                f.write('\n dir={0}/{1}/{2}\n out={3}.ts\n'.format(directory,num,i['cnt'],url[-27:]))
                files += '"{0}/{1}/{2}/{3}.ts"+'.format(directory,num,i['cnt'],url[-27:])
                break
            else:
                for ip in i['ip']:
                    url = 'http://{0}/segment.ts?cid={1}&var=orig&ts={2}.00-{3}.00'.format(ip,
                                                                                   i['uid'],
                                                                                   cur, cur+delta)
                    f.write('%s\t'%url)
                f.write('\n dir={0}/{1}/{2}\n out={3}.ts\n'.format(directory,num,i['cnt'],url[-27:]))

                files += '"{0}/{1}/{2}/{3}.ts"+'.format(directory,num,i['cnt'],url[-27:])

            cur+=delta

        #Генерируем метафайл для склеивания чанков в один большой файл.
        m=open('%s-%s.meta'%(num,i['cnt']),'w')
        m.write('MUXOPT --no-pcr-on-video-pid --new-audio-pes --vbr  --vbv-len=500\n')
        m.write('V_MPEG4/ISO/AVC, %s, fps=23.976, insertSEI, contSPS, track=3300\n'%files[:-1])
        m.write('A_AAC, %s, timeshift=-20ms, track=3301\n'%files[:-1])
        m.close()

        f.close()
        subprocess.Popen('aria2c.exe -i links-%s-%s.txt -d %s -x 16'%(num, i['cnt'], directory), shell=True).communicate()
        subprocess.Popen('tsMuxeR.exe %s-%s.meta %s/%s-%s.ts\n'%(num, i['cnt'], directory, num,i['cnt']), shell=True).communicate()
        shutil.rmtree('%s/%s'%(directory,num))
        os.remove('%s-%s.meta'%(num, i['cnt']))
        os.remove('links-%s-%s.txt'%(num, i['cnt']))


Опять же, код писался исключительно в целях проверки концепта и не является образцом соблюдения PEP8, но вполне работает. Скорость скачивания по понятным причинам зависит от многих факторов.

UPD Есть мнение, что старые камеры планомерно заменяют на новые. Вчера вечером было 337 старых и 5776 новых, сегодня утром — 273 старых, 5811 новых.

UPD Оказывается, есть еще и webvybory2013.ru, там с других выборов тоже картинка идет. Все что написано в этой статье, применимо и к ним, только домен поменять нужно.

UPD Камеры постоянно меняют свой статус, обратите на это внимание. Со старой системой заменяются на новые.
Tags:
Hubs:
+44
Comments 45
Comments Comments 45

Articles