2012-01-30 11 views
9

Mam problem i pytałem o to kilka razy, ale myślę, że jestem teraz o krok bliżej, więc mam nadzieję, że ktoś może mi pomóc reszta.Android ServerSocketSocket z plikami strumieniowymi jCIFS

Moje poprzednie pytania:

Mówiąc prościej - Chcę utworzyć aplikację, która:

  1. można podłączyć do urządzenia NAS przy użyciu jCIFS
  2. jest zdolny uruchomienia pliki w domyślnym widza - czyli film w odtwarzaczu wideo

Pierwsza część jest stosunkowo łatwe, a ja już zrobione, ale druga część jest to, co mnie i co trapi Pytałem już kilka razy wcześniej. Myślę, że zrobiłem pewne postępy.

Myślę, że muszę użyć w mojej aplikacji ServerSocket, aby w jakiś sposób utworzyć pomost między NAS a aplikacją odtwarzającą zawartość. Myślę, że można to zrobić za pomocą Service. Pliki z urządzenia NAS są dostępne jako FileInputStream.

Istnieje wiele aplikacji na rynku (tj. ES File Explorer), które są w stanie to zrobić bez dostępu root, więc wiem, że to możliwe - w tej chwili po prostu nie wiem jak.

Illustration of my idea

Szukałem na logcat podczas korzystania niektóre z wyżej wymienionych aplikacji, a wszystkie one wydają się być stworzenie serwera lokalnego, a następnie uruchomić wideo Intent z tego serwera. Jak można to osiągnąć?

Odpowiedz

18

Podstawową odpowiedzią jest użycie SmbFileInputStream do pobrania InputStream Prawdopodobnie używasz tego.

Teraz trudną częścią jest to, jak zaoferować InputStream innym aplikacjom.

Jednym z możliwych podejść, ile aplikacji zapewnia strumieniowanie dowolnego strumienia InputStream do innych aplikacji na urządzeniu, jest użycie schematu URL o numerze i tunelowanie strumienia przez http. Następnie aplikacje obsługujące adresy URL http mogą otwierać i wykorzystywać dane.

W tym celu należy utworzyć serwer http, co może wydawać się trudne, ale w rzeczywistości możliwe do zrealizowania. Dobrym źródłem na początek jest biblioteka nanohttpd, która jest tylko jednym źródłem java, pierwotnie używana do listy plików w katalogach, ale można ją dostosować do strumieniowania strumienia wejściowego przez http. To właśnie zrobiłem z sukcesem.

Twój adres URL będzie wyglądać tak: http: // localhost: 12345 gdzie 12345 jest portem, na którym twój serwer nasłuchuje żądań. Ten port można uzyskać z ServerSocket.getLocalPort(). Następnie podaj ten adres URL do niektórych aplikacji, a serwer czeka na połączenie i wysyła dane.

Uwaga dotycząca przesyłania strumieniowego HTTP: niektóre aplikacje (np. Odtwarzacze wideo), takie jak możliwe do wyświetlenia strumienie http (nagłówek zakresu http). Ponieważ możesz uzyskać również SmbRandomAccessFile, możesz sprawić, że twój mały serwer dostarczy dowolną część danych w pliku. Wbudowany odtwarzacz wideo dla systemu Android potrzebuje takiego dostępnego strumienia http, aby umożliwić wyszukiwanie w pliku wideo, w przeciwnym razie wystąpi błąd "Nie można odtwarzać wideo". Twój serwer musi być gotowy do obsługi rozłączeń i wielu połączeń z różnymi wartościami zasięgu.

Podstawowe zadania serwera http:

  1. tworzą ServerSocket
  2. utworzyć wątku oczekiwania na połączenie (Socket accept = serverSocket.accept()), jeden wątek może być ok, odkąd obsłużyć jednego klienta na czas
  3. żądania odczytu ((http) socket.getInputStream), głównie sprawdzić metodą GET i Range nagłówek)
  4. wysłać nagłówki, głównie Content-Type, Content-Length, Accept-zakresach nagłówków Content-klasy
  5. wysłać rzeczywiste dane binarne, które jest proste kopiowanie InputStream (plików) do OutputStream (gniazdo) rozłącza
  6. obsłudze, błędy, wyjątków

powodzenia w realizacji.

EDYTOWANIE:

Oto moja klasa, która robi rzecz. Odwołuje się do niektórych nieistniejących klas dla pliku, które powinny być trywialne, aby zastąpić je klasą pliku.

/** 
* 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; 
    } 
} 
+0

Dzięki myszy! To wygląda świetnie. Znalazłem coś, co wspomniało nanohttpd, i wygląda całkiem nieźle. +1 na teraz :-) –

+0

Zaskoczyłem to od jakiegoś czasu i nie mogę sprawić, żeby to działało. Wypróbowałeś też twoją aplikację X-plore z moim NAS w domu i chociaż łączy się, nie mogę odtwarzać (streamować) żadnego z filmów. Działa świetnie w eksploratorze plików ES. –

+0

Jaki typ pliku (rozszerzenie) testujesz, na którym odtwarzaczu? –

1

Samsung S5 (Android wersja 5.1.1), to w obliczu problemu życzenie zakres począwszy od wartości większej niż rozmiar pliku i rozwiązać go przez ustawienie statusu = „200 OK”, jak poniżej:

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"; 
} 

pozostałe nagłówki zostały pozostawione jako nowy wniosek o strumieniu

Powiązane problemy