12

I've got a bit of an issue and I've been asking regarding it quite a few times, but I think I'm one step closer now, so hopefully someone can help me with the rest.

My previous questions:

Put simply - I want to create an application that:

  1. Can connect to a NAS device using jCIFS
  2. Is capable of launching files in the default viewer - i.e. a video in the video player

The first part is relatively easy and I've already done that, but the second part is what's troubling me and what I've asked about a few times before. I think I've made some progress though.

I think I need to use a ServerSocket in my application to somehow create a bridge between the NAS and the application that's playing the content. I'm thinking this could be done using a Service. The files from the NAS device can be accessed as a FileInputStream.

There are plenty of applications on Market (i.e. ES File Explorer) that are capable of doing this without root access, so I know it's possible - at the moment I just don't know how.

Illustration of my idea

I've been looking at Logcat while using some of the aforementioned applications, and they all seem to be creating a local server and then launch a video Intent from that server. How can this be achieved?

Community
  • 1
  • 1
Michell Bak
  • 13,182
  • 11
  • 64
  • 121

2 Answers2

21

Basic answer is to use SmbFileInputStream to get InputStream You probably use this.

Now the tricky part is how to offer InputStream to other apps.

One possible approach, how many apps provide streaming of any InputStream to other apps on device, is to use http: URL scheme, and tunel your stream over http. Then apps that can handle http URLs can open and use your data.

For this you have to make some kind of http server, which sounds difficult, but actually is achievable task. Good source to start with is nanohttpd library which is just one java source, originally used to list files in dirs, but you can adapt it to stream your InputStream over http. That's what I did with success.

Your url would look like http:// localhost:12345 where 12345 is port on which your server listens for requests. This port may be obtained from ServerSocket.getLocalPort(). Then give this URL to some app and your server waits for connection and sends data.

A note about http streaming: some apps (e.g. video players) like seekable http streams (http Range header). Since you can get also SmbRandomAccessFile, you can make your tiny server to provide any part of data in file. Android's built-in video player needs such seekable http stream in order to allow seeking in video file, otherwise it gives "Video can't be played" error. Your server must be ready to handle disconnects and multiple connects with different Range values.

Basic tasks of http server:

  1. create ServerSocket
  2. create Thread waiting for connection (Socket accept = serverSocket.accept()), one thread may be ok since you'd handle single client at a time
  3. read http request (socket.getInputStream()), mainly check GET method and Range header)
  4. send headers, mainly Content-Type, Content-Length, Accept-Ranges, Content-Range headers
  5. send actual binary data, which is plain copying of InputStream (file) to OutputStream (socket)
  6. handle disconnects, errors, exceptions

Good luck in implementation.

EDIT:

Here's my class that does the thing. It references some non-present classes for file, which should be trivial for you to replace by your file class.

/**
 * 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;
   }
}
Freek de Bruijn
  • 3,552
  • 2
  • 22
  • 28
Pointer Null
  • 39,597
  • 13
  • 90
  • 111
  • Thanks, mice! That looks awesome. I found something else mentioning nanohttpd, and that looks pretty good. +1 for now :-) – Michell Bak Feb 01 '12 at 15:14
  • I've been messing with this for a while now, and I can't seem to get it working. Also tried your application X-plore with my NAS at home, and although it connects, I can't play (stream) any of the videos. It works brilliantly in ES file explorer. – Michell Bak Feb 21 '12 at 13:34
  • Which file type (extension) do you test, on which player? – Pointer Null Feb 21 '12 at 16:32
  • I'm testing with .avi and .mkv files. I've tried to open them in both Dice Player and MX Video Player - both players are perfectly capable of handling the codecs and HTTP streaming. Again, works in ES file explorer. – Michell Bak Feb 21 '12 at 18:10
  • You're right, Dice Player stops. Other players work with some file types, and not with others. Problem is that writing to socket suddenly blocks (see http://elonen.iki.fi/code/nanohttpd/NanoHTTPD.java.html and call to out.write) after writing some 1548288 bytes. I copy stream in buffer of size 8192 bytes. Any idea why other party fails to read data being sent? – Pointer Null Feb 22 '12 at 08:08
  • I found my problem: my server had just one thread (listening for connection + sending data), thus could serve only one request at time. After changing it to run each request in own thread, this fixed mentioned problems with streaming. This means that some players open multiple connections to stream url, and your server must be ready to respond to more requests in parallel. This also means that each response thread must open its own copy of file input stream. – Pointer Null Feb 22 '12 at 12:30
  • Oh, awesome! I know this is a bit much to ask for, but is it possible for me to see your complete implementation of handling video files on a network connection? I think I've hit a dead end. I'm still learning things, and I'd really like to see how a well-constructed solution looks. I know X-plore from the UIQ days and really enjoyed using it back then. I'm only asking because I know that X-plore is freeware. E-mail would be great if you don't want to post it publically. – Michell Bak Feb 22 '12 at 16:46
  • You're absolutely brilliant - thanks so much! I'm hopeful this will help not only me, but a lot of other people as well :) – Michell Bak Feb 23 '12 at 11:54
  • Hi again! I hope you don't mind me bringing this up again, but I do have one last question regarding your code. The file references are quite trivial, but you're referencing a class called RandomAccessInputStream, which seems a bit more complex. I've been googling to see if this was an open-source class you've used, but I've simply found too many different classes to make out anything for sure, so I thought I'd ask you regarding it. – Michell Bak Mar 03 '12 at 16:28
  • That's my class, I added it to end of code. You need to implement some bits of it to make it work on a file. – Pointer Null Mar 05 '12 at 08:50
  • This is getting pretty old, I know, but for some weird reason I can't seem to get it working. It seems my InputStream from the SmbFile works as expected and the server is up and running, but I can't play any videos using the Intent.ACTION_VIEW and "video/*" mimetype. Any ideas? I really appreciate your help on this, I haven't got any prior experience with coding server functionality. – Michell Bak Mar 27 '12 at 00:02
  • You got all the code, now you need to learn debugging ;) Use logs, breakpoints, etc. – Pointer Null Mar 27 '12 at 08:41
  • @henry4343: FileEntry is my complicated class to keep info about file. This code is example and won't compile on itself, you can just learn from it and adjust to your needs. – Pointer Null Jan 06 '14 at 07:50
  • http://stackoverflow.com/questions/20945482/whats-different-between-sambafileinputstream-and-fileinputstream this is my question. Can you give me a suggest. I already get http address but it didn't work because fileinputstream not equal smbfileinputstream – henry4343 Jan 06 '14 at 08:17
  • Thanks Pointer Null, all I need to stream video from PC is here. After some debugging i only had to do some minor changes to your server code and it works like a charm ! – Larphoid Jun 15 '14 at 17:30
  • Hi, I would like to know if I can enter any random port numbers in here to stream the video, or are there any specific port numbers for each particular thing that I have to put in? Sorry for the noob question, but I would just like to know because I am new in this networking area. Thank you – AuroraBlaze Sep 29 '14 at 09:10
  • Any port can be used. ServerSocket(0) assigns unused port. – Pointer Null Oct 02 '14 at 08:26
  • I'm trying to test this, but can't make it work yet. – Ivan Verges Oct 10 '14 at 20:17
  • Hi, I dont know what Browser.FileEntry is ? Can you help me – Moti Jun 28 '17 at 07:36
  • 1
    @Moti: that's my data source representing file. Study the code and replace it by yours. – Pointer Null Jun 28 '17 at 09:58
  • @ Pointer Null Thanks! – Moti Jul 04 '17 at 08:40
  • @Pointer Null This code will work with live streaming? (from byte array to video)? – Moti Jul 10 '17 at 09:25
  • @Moti: It's unclear what you mean by live streaming. This is http server, use it for that purpose. – Pointer Null Jul 24 '17 at 10:15
  • b[0]&0xff is for conversion from byte to int right? Why not just return b[0] ? – ed22 Feb 14 '18 at 13:29
  • @ed22: due to negative numbers, you don't want byte 0xff be returned as int -1 (which is EOF marker) but as int 255 – Pointer Null Feb 14 '18 at 19:46
  • @Pointer Null, still, why 255 is better than -1 ? Why is EOF bad? – ed22 Feb 15 '18 at 12:38
  • Oh, these are too basic questions. Learn something about signed/unsigned values, conversion from byte to int, and design of InputStream, which delegates value -1 as EOF marker, and that read() shall return values >= 0. – Pointer Null Feb 15 '18 at 14:16
1

In Samsung S5 (Android version 5.1.1), I faced a problem of range request starting from a value greater than the file size and I solved it by setting status = "200 OK" as below:

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

The remaining headers were left as a fresh request for the stream