Android ServerПроцессирование сокетов с потоковыми файлами jCIFS - PullRequest
12 голосов
/ 30 января 2012

У меня есть небольшая проблема, и я спрашивал об этом довольно много раз, но я думаю, что теперь я на шаг ближе, так что, надеюсь, кто-то может помочь мне с остальными.

Мои предыдущие вопросы:

Проще говоря - я хочу создать приложение, которое:

  1. Можно подключиться к устройству NAS с помощью jCIFS
  2. Может запускать файлы в программе просмотра по умолчанию - то есть видео в проигрывателе видео

Первая часть относительно проста, и я уже сделал это, но вторая часть - это то, что беспокоит меня и то, о чем я спрашивал несколько раз раньше. Я думаю, что я добился определенного прогресса.

Я думаю, что мне нужно использовать ServerSocket в моем приложении, чтобы каким-то образом создать мост между NAS и приложением, которое воспроизводит контент. Я думаю, что это можно сделать с помощью Service. Файлы с устройства NAS доступны как FileInputStream.

Существует множество приложений на Маркете (например, ES File Explorer ), которые способны делать это без root-доступа, поэтому я знаю, что это возможно - на данный момент я просто не знаю, как.

Illustration of my idea

Я смотрел Logcat при использовании некоторых из вышеупомянутых приложений, и все они, кажется, создают локальный сервер, а затем запускают видео Intent с этого сервера. Как этого достичь?

Ответы [ 2 ]

20 голосов
/ 01 февраля 2012

Основной ответ - использовать SmbFileInputStream для получения InputStream Возможно, вы используете это.

Теперь самое сложное - как предложить InputStream другим приложениям.

Один из возможных подходов - сколько приложений обеспечивают потоковую передачу любого InputStream другим приложениям на устройстве - использовать http: схему URL-адресов и настраивать поток через http. Тогда приложения, которые могут обрабатывать URL-адреса http, могут открывать и использовать ваши данные.

Для этого вам нужно создать некий http-сервер, который звучит сложно, но на самом деле является достижимой задачей. Хороший источник для начала - это nanohttpd библиотека, которая является всего лишь одним источником Java, изначально использовалась для вывода списка файлов в директории, но вы можете адаптировать ее для потоковой передачи вашего InputStream через http. Это то, что я сделал с успехом.

Ваш URL будет выглядеть как http: // localhost: 12345, где 12345 - это порт, на котором ваш сервер прослушивает запросы. Этот порт может быть получен из ServerSocket.getLocalPort (). Затем передайте этот URL-адрес какому-либо приложению, и ваш сервер ожидает соединения и отправляет данные.

Примечание о потоковой передаче по протоколу http: некоторые приложения (например, видеопроигрыватели), например, доступные для поиска потоки http (заголовок диапазона HTTP). Так как вы можете также получить SmbRandomAccessFile, вы можете сделать свой крошечный сервер для предоставления любой части данных в файле. Встроенный видеопроигрыватель Android нуждается в таком доступном http-потоке, чтобы разрешить поиск в видеофайле, в противном случае выдает ошибку «Невозможно воспроизвести видео». Ваш сервер должен быть готов обрабатывать разъединения и множественные соединения с различными значениями Range.

Основные задачи http-сервера:

  1. создать ServerSocket
  2. создать поток, ожидающий соединения (Socket accept = serverSocket.accept ()), один поток может быть в порядке, так как вы будете обрабатывать один клиент за раз
  3. чтение http запроса (socket.getInputStream ()), в основном проверка метода GET и заголовка Range)
  4. отправлять заголовки, в основном Content-Type, Content-Length, Accept-Ranges, Content-Range headers
  5. отправляет фактические двоичные данные, которые являются простым копированием InputStream (файла) в OutputStream (сокет)
  6. обрабатывать разъединения, ошибки, исключения

Удачи в реализации.

EDIT:

Вот мой класс, который делает это. Он ссылается на некоторые несуществующие классы для файла, который должен быть тривиальным для замены вашим классом файла.

/**
 * This is simple HTTP local server for streaming InputStream to apps which are capable to read data from url.
 * Random access input stream is optionally supported, depending if file can be opened in this mode. 
 */
public class StreamOverHttp{
   private static final boolean debug = false;

   private final Browser.FileEntry file;
   private final String fileMimeType;

   private final ServerSocket serverSocket;
   private Thread mainThread;

   /**
    * Some HTTP response status codes
    */
   private static final String 
      HTTP_BADREQUEST = "400 Bad Request",
      HTTP_416 = "416 Range not satisfiable",
      HTTP_INTERNALERROR = "500 Internal Server Error";

   public StreamOverHttp(Browser.FileEntry f, String forceMimeType) throws IOException{
      file = f;
      fileMimeType = forceMimeType!=null ? forceMimeType : file.mimeType;
      serverSocket = new ServerSocket(0);
      mainThread = new Thread(new Runnable(){
         @Override
         public void run(){
            try{
               while(true) {
                  Socket accept = serverSocket.accept();
                  new HttpSession(accept);
               }
            }catch(IOException e){
               e.printStackTrace();
            }
         }

      });
      mainThread.setName("Stream over HTTP");
      mainThread.setDaemon(true);
      mainThread.start();
   }

   private class HttpSession implements Runnable{
      private boolean canSeek;
      private InputStream is;
      private final Socket socket;

      HttpSession(Socket s){
         socket = s;
         BrowserUtils.LOGRUN("Stream over localhost: serving request on "+s.getInetAddress());
         Thread t = new Thread(this, "Http response");
         t.setDaemon(true);
         t.start();
      }

      @Override
      public void run(){
         try{
            openInputStream();
            handleResponse(socket);
         }catch(IOException e){
            e.printStackTrace();
         }finally {
            if(is!=null) {
               try{
                  is.close();
               }catch(IOException e){
                  e.printStackTrace();
               }
            }
         }
      }

      private void openInputStream() throws IOException{
         // openRandomAccessInputStream must return RandomAccessInputStream if file is ssekable, null otherwise
         is = openRandomAccessInputStream(file);
         if(is!=null)
            canSeek = true;
         else
            is = openInputStream(file, 0);
      }

      private void handleResponse(Socket socket){
         try{
            InputStream inS = socket.getInputStream();
            if(inS == null)
               return;
            byte[] buf = new byte[8192];
            int rlen = inS.read(buf, 0, buf.length);
            if(rlen <= 0)
               return;

            // Create a BufferedReader for parsing the header.
            ByteArrayInputStream hbis = new ByteArrayInputStream(buf, 0, rlen);
            BufferedReader hin = new BufferedReader(new InputStreamReader(hbis));
            Properties pre = new Properties();

            // Decode the header into params and header java properties
            if(!decodeHeader(socket, hin, pre))
               return;
            String range = pre.getProperty("range");

            Properties headers = new Properties();
            if(file.fileSize!=-1)
               headers.put("Content-Length", String.valueOf(file.fileSize));
            headers.put("Accept-Ranges", canSeek ? "bytes" : "none");

            int sendCount;

            String status;
            if(range==null || !canSeek) {
               status = "200 OK";
               sendCount = (int)file.fileSize;
            }else {
               if(!range.startsWith("bytes=")){
                  sendError(socket, HTTP_416, null);
                  return;
               }
               if(debug)
                  BrowserUtils.LOGRUN(range);
               range = range.substring(6);
               long startFrom = 0, endAt = -1;
               int minus = range.indexOf('-');
               if(minus > 0){
                  try{
                     String startR = range.substring(0, minus);
                     startFrom = Long.parseLong(startR);
                     String endR = range.substring(minus + 1);
                     endAt = Long.parseLong(endR);
                  }catch(NumberFormatException nfe){
                  }
               }

               if(startFrom >= file.fileSize){
                  sendError(socket, HTTP_416, null);
                  inS.close();
                  return;
               }
               if(endAt < 0)
                  endAt = file.fileSize - 1;
               sendCount = (int)(endAt - startFrom + 1);
               if(sendCount < 0)
                  sendCount = 0;
               status = "206 Partial Content";
               ((RandomAccessInputStream)is).seek(startFrom);

               headers.put("Content-Length", "" + sendCount);
               String rangeSpec = "bytes " + startFrom + "-" + endAt + "/" + file.fileSize;
               headers.put("Content-Range", rangeSpec);
            }
            sendResponse(socket, status, fileMimeType, headers, is, sendCount, buf, null);
            inS.close();
            if(debug)
               BrowserUtils.LOGRUN("Http stream finished");
         }catch(IOException ioe){
            if(debug)
               ioe.printStackTrace();
            try{
               sendError(socket, HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
            }catch(Throwable t){
            }
         }catch(InterruptedException ie){
            // thrown by sendError, ignore and exit the thread
            if(debug)
               ie.printStackTrace();
         }
      }

      private boolean decodeHeader(Socket socket, BufferedReader in, Properties pre) throws InterruptedException{
         try{
            // Read the request line
            String inLine = in.readLine();
            if(inLine == null)
               return false;
            StringTokenizer st = new StringTokenizer(inLine);
            if(!st.hasMoreTokens())
               sendError(socket, HTTP_BADREQUEST, "Syntax error");

            String method = st.nextToken();
            if(!method.equals("GET"))
               return false;

            if(!st.hasMoreTokens())
               sendError(socket, HTTP_BADREQUEST, "Missing URI");

            while(true) {
               String line = in.readLine();
               if(line==null)
                  break;
   //            if(debug && line.length()>0) BrowserUtils.LOGRUN(line);
               int p = line.indexOf(':');
               if(p<0)
                  continue;
               final String atr = line.substring(0, p).trim().toLowerCase();
               final String val = line.substring(p + 1).trim();
               pre.put(atr, val);
            }
         }catch(IOException ioe){
            sendError(socket, HTTP_INTERNALERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
         }
         return true;
      }
   }


   /**
    * @param fileName is display name appended to Uri, not really used (may be null), but client may display it as file name.
    * @return Uri where this stream listens and servers.
    */
   public Uri getUri(String fileName){
      int port = serverSocket.getLocalPort();
      String url = "http://localhost:"+port;
      if(fileName!=null)
         url += '/'+URLEncoder.encode(fileName);
      return Uri.parse(url);
   }

   public void close(){
      BrowserUtils.LOGRUN("Closing stream over http");
      try{
         serverSocket.close();
         mainThread.join();
      }catch(Exception e){
         e.printStackTrace();
      }
   }

   /**
    * Returns an error message as a HTTP response and
    * throws InterruptedException to stop further request processing.
    */
   private static void sendError(Socket socket, String status, String msg) throws InterruptedException{
      sendResponse(socket, status, "text/plain", null, null, 0, null, msg);
      throw new InterruptedException();
   }

  private static void copyStream(InputStream in, OutputStream out, byte[] tmpBuf, long maxSize) throws IOException{

     while(maxSize>0){
        int count = (int)Math.min(maxSize, tmpBuf.length);
        count = in.read(tmpBuf, 0, count);
        if(count<0)
           break;
        out.write(tmpBuf, 0, count);
        maxSize -= count;
     }
  }
   /**
    * Sends given response to the socket, and closes the socket.
    */
   private static void sendResponse(Socket socket, String status, String mimeType, Properties header, InputStream isInput, int sendCount, byte[] buf, String errMsg){
      try{
         OutputStream out = socket.getOutputStream();
         PrintWriter pw = new PrintWriter(out);

         {
            String retLine = "HTTP/1.0 " + status + " \r\n";
            pw.print(retLine);
         }
         if(mimeType!=null) {
            String mT = "Content-Type: " + mimeType + "\r\n";
            pw.print(mT);
         }
         if(header != null){
            Enumeration<?> e = header.keys();
            while(e.hasMoreElements()){
               String key = (String)e.nextElement();
               String value = header.getProperty(key);
               String l = key + ": " + value + "\r\n";
//               if(debug) BrowserUtils.LOGRUN(l);
               pw.print(l);
            }
         }
         pw.print("\r\n");
         pw.flush();
         if(isInput!=null)
            copyStream(isInput, out, buf, sendCount);
         else if(errMsg!=null) {
            pw.print(errMsg);
            pw.flush();
         }
         out.flush();
         out.close();
      }catch(IOException e){
         if(debug)
            BrowserUtils.LOGRUN(e.getMessage());
      }finally {
         try{
            socket.close();
         }catch(Throwable t){
         }
      }
   }
}

/**
 * Seekable InputStream.
 * Abstract, you must add implementation for your purpose.
 */
abstract class RandomAccessInputStream extends InputStream{

   /**
    * @return total length of stream (file)
    */
   abstract long length();

   /**
    * Seek within stream for next read-ing.
    */
   abstract void seek(long offset) throws IOException;

   @Override
   public int read() throws IOException{
      byte[] b = new byte[1];
      read(b);
      return b[0]&0xff;
   }
}
1 голос
/ 07 апреля 2016

В Samsung S5 (версия Android 5.1.1) я столкнулся с проблемой запроса диапазона, начиная со значения, превышающего размер файла, и решил ее, установив status = "200 OK", как показано ниже:

if (startFrom >= contentLength) {
    // when you receive a request from MediaPlayer that does not contain Range in the HTTP header , then it is requesting a new stream
    // https://code.google.com/p/android/issues/detail?id=3031
    status = "200 OK";
}

Оставшиеся заголовки были оставлены как новый запрос для потока

...