вторник, 22 мая 2012 г.

FLV и YouTube

Недавно исследовал сабж, выношу результаты в своего рода FAQ:

Где быстро и просто узнать о FLV?
На сайте Xuggler есть 4 коротких обучающих видео по 5 минут. Покрывают все основы, единственный вопрос который там остается без внимания, это кодеки в видеопотоке FLV.

Какие кодеки можно использовать в FLV видеостриме?
Написано в этой статье:
Flash Player versionReleasedFile formatVideo compression formatsAudio compression formats
62002SWFSorenson Spark, Screen videoMP3, ADPCM, Nellymoser
72003SWF, FLVSorenson Spark, Screen videoMP3, ADPCM, Nellymoser
82005SWF, FLVOn2 VP6, Sorenson Spark, Screen video, Screen video 2MP3, ADPCM, Nellymoser
92007SWF, FLVOn2 VP6, Sorenson Spark, Screen video, Screen video 2, H.264MP3, ADPCM, Nellymoser, AAC
SWF, F4V, ISO base media file formatH.264AAC, MP3
102008SWF, FLVOn2 VP6, Sorenson Spark, Screen video, Screen video 2, H.264MP3, ADPCM, Nellymoser, Speex, AAC
SWF, F4V, ISO base media file formatH.264AAC, MP3
Можно ли скачивать видео с YouTube с помощью YouTube Java API?
Нельзя. TOS хоть прямо и не говорит, но намекает что ютубу нет смысла публично заявлять о возможности это делать.

Можно ли скачивать видео с YouTube?
Можно, для этого надо использовать REST апи ютуба (вот они, двойные стандарты!). Это API нещадно меняют время от времени, но суть остается:
1) Первым запросом получаем мета-инфу о видео
2) Вторым запросом (где используем инфу из ответа на первый) запрашиваем файл.

В данный момент это делается с использованием таких методов REST api:
get_video_info, в ответе которого хранится интересующее нас поле url_encoded_fmt_stream_map.
В этом поле содержится несколько ссылок на различные качества видео, а чтобы ссылка была дееспособна, надо отсекать от неё все что идет после "fallback_host" включительно.

Ну и немаловажный момент: первый запрос и второй должны иметь общий http контекст, так как в первом запросе в контексте создается кука, без которой второй запрос получит 403 от сервера.

Вот proof-of-concept код, достаточно корявый но рабочий:


import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;


import java.util.*;


import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;


public class D {


    private static final String scheme = "http";
    private static final String host = "www.youtube.com";


    public static void main(String[] args) {
        try {
            String videoId = null;
            String outdir = ".";
            if (args.length == 1) {
                videoId = args[0];
            } else if (args.length == 2) {
                videoId = args[0];
                outdir = args[1];
            }


            int format = 18; // http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs
            String encoding = "UTF-8";
            String userAgent = "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13";
            File outputDir = new File(outdir);
            play(videoId, format, encoding, userAgent, outputDir);


        } catch (Throwable t) {
            t.printStackTrace();
        }
    }


    static HttpContext localContext = new BasicHttpContext();
    private static void play(String videoId, int format, String encoding, String userAgent, File outputdir) throws Throwable {
        System.out.println("Retrieving " + videoId);
        List qparams = new ArrayList();
        qparams.add(new BasicNameValuePair("video_id", videoId));
        qparams.add(new BasicNameValuePair("fmt", "" + format));
        URI uri = getUri("get_video_info", qparams);


        CookieStore cookieStore = new BasicCookieStore();
        localContext = new BasicHttpContext();
        localContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);


        HttpClient httpclient = new DefaultHttpClient();
        HttpGet httpget = new HttpGet(uri);
        httpget.setHeader("User-Agent", userAgent);


        System.out.println("Executing " + uri);
        HttpResponse response = httpclient.execute(httpget, localContext);
        HttpEntity entity = response.getEntity();
        if (entity != null && response.getStatusLine().getStatusCode() == 200) {
            InputStream instream = entity.getContent();
            String videoInfo = getStringFromInputStream(encoding, instream);
            if (videoInfo != null && videoInfo.length() > 0) {
                List infoMap = new ArrayList();
                URLEncodedUtils.parse(infoMap, new Scanner(videoInfo), encoding);
                String downloadUrl = null;
                String filename = videoId;
                String urlMap = null;


                for (NameValuePair pair : infoMap) {
                    String key = pair.getName();
                    String val = pair.getValue();
                    System.out.println(key + "=" + val);
                    if (key.equals("title")) {
                        filename = val;
                    } else if (key.equals("url_encoded_fmt_stream_map")) {
                        urlMap = val;
                        String[] urls = urlMap.split("url=");
                        String[] decodedUrls = new String[urls.length];
                        for (int i = 0; i < urls.length; i++) {
                            decodedUrls[i] = URLDecoder.decode(URLDecoder.decode(urls[i], "UTF-8"), "UTF-8").replaceAll(",$", "");
                        }
                        //we use first available quality
                        downloadUrl = decodedUrls[1].replaceAll("&fallback_host.*", "");
                    }
                }
                File outputfile = new File(outputdir, "out.flv");
                if (downloadUrl != null) {
                    downloadWithHttpClient(userAgent, downloadUrl, outputfile);
                }
            }
        }


    }


    private static void downloadWithHttpClient(String userAgent, String downloadUrl, File outputfile) throws Throwable {
        HttpGet httpget2 = new HttpGet(downloadUrl);
        httpget2.setHeader("User-Agent", userAgent);
        System.out.println("Executing " + httpget2.getURI());
        HttpClient httpclient2 = new DefaultHttpClient();
        HttpResponse response2 = httpclient2.execute(httpget2, localContext);
        HttpEntity entity2 = response2.getEntity();
        if (entity2 != null && response2.getStatusLine().getStatusCode() == 200) {
            long length = entity2.getContentLength();
            InputStream instream2 = entity2.getContent();
            System.out.println("Writing " + length + " bytes to " + outputfile);
            if (outputfile.exists()) {
                outputfile.delete();
            }
            FileOutputStream outstream = new FileOutputStream(outputfile);
            try {
                byte[] buffer = new byte[2048];
                int count = -1;
                while ((count = instream2.read(buffer)) != -1) {
                    outstream.write(buffer, 0, count);
                }
                outstream.flush();
            } finally {
                outstream.close();
            }
        }
    }


    private static URI getUri(String path, List qparams) throws URISyntaxException {
        URI uri = URIUtils.createURI(scheme, host, -1, "/" + path, URLEncodedUtils.format(qparams, "UTF-8"), null);
        return uri;
    }


    private static String getStringFromInputStream(String encoding, InputStream instream) throws UnsupportedEncodingException, IOException {
        Writer writer = new StringWriter();


        char[] buffer = new char[1024];
        try {
            Reader reader = new BufferedReader(new InputStreamReader(instream, encoding));
            int n;
            while ((n = reader.read(buffer)) != -1) {
                writer.write(buffer, 0, n);
            }
        } finally {
            instream.close();
        }
        String result = writer.toString();
        return result;
    }
}




Комментариев нет: