Java媒体框架(JMF)使你能够编写出功能强大的多媒体程序,却不用关心底层复杂的实现细节。JMF  API的使用相对比较简单,但是能够满足几乎所有多媒体编程的需求。在这篇文章中,我将向你介绍如何用很少的代码就编写出多媒体程序。
  
  Java多媒体框架(JMF)中包含了许多用于处理多媒体的API。它是一个相当复杂的系统,完全了解这个系统可能需要花上几周的时间,但是这篇文章将主要介绍JMF的几个核心接口和类,然后通过一个简单的例子向你展示如何利用该接口进行编程。
  
  JMF目前的最新版本是2.1,Sun通过它向Java中引入处理多媒体的能力。下面是JMF所支持的功能的一个概述:
  
  ● 可以在Java Applet和应用程序中播放各种媒体文件,例如AU、AVI、MIDI、MPEG、QuickTime和WAV等文件。
  
  ● 可以播放从互联网上下载的媒体流。
  
  ● 可以利用麦克风和摄像机一类的设备截取音频和视频,并保存成多媒体文件。
  
  ● 处理多媒体文件,转换文件格式。
  
  ● 向互联网上传音频和视频数据流。
  
  ● 在互联网上广播音频和视频数据。
  JMF的结构
  为了更好地说明JMF的结构,让我们用立体声音响做一个简单的比喻。当你CD机播放CD唱片的时候,CD唱片向系统提供音乐信号。这些数据是在录音棚中用麦克风和其他类似的设备记录下来的。CD播放机将音乐信号传送到系统的音箱上。在这个例子中,麦克风就是一个音频截取设备,CD唱片是数据源,而音箱是输出设备。
  
  JMF的结构和立体声音响系统非常相似,在后面的文章中,你会遇到下面的这些术语:
  ● 数据源(Data source)
  
  ● 截取设备(Capture Device,包括视频和音频截取设备)
  
  ● 播放器(Player)
  
  ● 处理器(Processor)
  
  ● 数据格式(Format)
  
  ● 管理器(Manager)
  
  下面让我们来看一看这些术语到底代表什么意思。
  
  1.数据源
  
  就像CD中保存了歌曲一样,数据源中包含了媒体数据流。在JMF中,DataSource对象就是数据源,它可以是一个多媒体文件,也可以是从互联网上下载的数据流。对于DataSource对象,一旦你确定了它的位置和类型,对象中就包含了多媒体的位置信息和能够播放该多媒体的软件信息。当创建了DataSource对象后,可以将它送入Player对象中,而Player对象不需要关心DataSource中的多媒体是如何获得的,以及格式是什么。
  
  在某些情况下,你需要将多个数据源合并成一个数据源。例如当你在制作一段录像时,你需要将音频数据源和视频数据源合并在一起。JMF支持数据源合并,在后面的例子中我们将提到这一点。
  
  2.截取设备
  
  截取设备指的是可以截取到音频或视频数据的硬件,如麦克风、摄像机等。截取到的数据可以被送入Player对象中进行处理。
  
  3.播放器
  
  在JMF中对应播放器的接口是Player。Player对象将音频/视频数据流作为输入,然后将数据流输出到音箱或屏幕上,就像CD播放机读取CD唱片中的歌曲,然后将信号送到音箱上一样。Player对象有多种状态,JMF中定义了JMF的六种状态,在正常情况下Player对象需要经历每个状态,然后才能播放多媒体。下面是对这些状态的说明。
  
  ● Unrealized:在这种状态下,Player对象已经被实例化,但是并不知道它需要播放的多媒体的任何信息。
  
  ●  Realizing:当调用realize()方法时,Player对象的状态从Unrealized转变为Realizing。在这种状态下,Player对象正在确定它需要占用哪些资源。
  ● Realized:在这种状态下Player对象已经确定了它需要哪些资源,并且也知道需要播放的多媒体的类型。
  ●  Prefetching:当调用prefectch()方法时,Player对象的状态从Realized变为Prefetching。在该状态下的Player对象正在为播放多媒体做一些准备工作,其中包括加载多媒体数据,获得需要独占的资源等。这个过程被称为预取(Prefetch)。
  
  ● Prefetched:当Player对象完成了预取操作后就到达了该状态。
  
  ●  Started:当调用start()方法后,Player对象就进入了该状态并播放多媒体。
  4.处理器
  处理器对应的接口是Processor,它一种播放器。在JMF API中,Processor接口继承了Player接口。 Processor对象除了支持支持Player对象支持的所有功能,还可以控制对于输入的多媒体数据流进行何种处理以及通过数据源向其他的Player对象或Processor对象输出数据。
  除了在播放器中提到了六种状态外,Processor 对象还包括两种新的状态,这两种状态是在Unrealized状态之后,但是在Realizing状态之前。
  ●  Configuring:当调用configure()方法后,Processor对象进入该状态。在该状态下,Processor对象连接到数据源并获取输入数据的格式信息。
  ● Configured:当完成数据源连接,获得输入数据格式的信息后,Processor对象就处于Configured状态。
  5.数据格式
  Format对象中保存了多媒体的格式信息。该对象中本身没有记录多媒体编码的相关信息,但是它保存了编码的名称。Format的子类包括AudioFormat和VideoFormat类,ViedeoFomat又有六个子类:H261Format、H263Format、IndexedColorFormat、JPEGFormat、RGBFormat和YUVFormat类。
  6.管理器
  JMF提供了下面四种管理器:
  ●  Manager:Manager相当于两个类之间的接口。例如当你需要播放一个DataSource对象,你可以通过使用Manager对象创建一个Player对象来播放它。使用Manager对象可以创建Player、Processor、DataSource和DataSink对象。
  ● PackageManager:该管理器中保存了JMF类注册信息。
  ●  CaptureDeviceManager:该管理器中保存了截取设备的注册信息。
  ●  PlugInManager:该管理器中保存了JMF插件的注册信息。  创建一个Player对象
  在JMF编程中,最常见的工作就是创建一个Player对象。你可以通过Manager类的createPlayer()方法创建Player对象。Manager对象使用多媒体的URL或MediaLocator对象来创建Player对象。当你获得了一个Player对象后,你可以通过调用getVisualComponent()方法得到Player对象的图像部件(Visual  Component,在图像部件上可以播放多媒体的图像)。然后将图像部件加入到应用程序或Applet的界面上。Player对象还包含一个控制面板,在上面可以控制媒体的播放、停止和暂停等。
  Player类中的很多方法只有在Player对象处于Realized的状态下才会被调用。为了保证Player对象已经到达了该状态,你需要使用Manager的createRealizePlayer()方法来获得Player对象。但是对于start()方法来说,你可以在Player对象到达Prefetched状态之前调用它,它可以自动将Player的状态转换到Started状态。
  截取多媒体数据
  多媒体数据的截取是JMF程序中另一个非常重要的功能。你可以按照下面的步骤截取数据:
  
  ● 通过查询CaptureDevieceManager获得你希望使用的截取设备。
  
  ● 获得设备对应的CaptureDeviceInfo对象。
  
  ● 从CaptureDeviecInfo对象中获得MediaLocator对象,然后用它创建一个DataSource对象。
  
  ● 使用DataSource对象创建Player对象或Processor对象。
  
  ● 调用start()方法,开始截取多媒体数据。
  
  你可以使用CaptureDeviceManager对象获得系统中可用的视频和音频截取设备。通过调用getDeviceList()方法你可以获得设备的列表。每个设备都对应一个CaptrueDeviceInfo对象。也可以通过调用CaptureDevieceManager对象的getDevice()方法来获得特定的CaptureDeviceInfo对象。在使用设备截取多媒体数据前,还需要从CaptureDeviceInfo对象中获得设备对应的MediaLocator对象。然后你可以直接使用MediaLocator来构造Player或Processor的实例,也可以用MediaLocator构造一个DataSource对象,然后将DataSource对象送入Player或Processor对象中。最后调用start()方法来截取多媒体数据。
  一个JMF例子
  当你使用JMF进行编程以前,你需要安装JMF。同时在硬件上也有一些要求。由于本文的代码是在Windows  2000下编写和测试,因此文章中提到的操作系统需要的软件都是与Windows有关的。虽然Java是跨平台的,但是JMF是个例外――并不是所有的平台上都实现了JMF。
  
  硬件和软件要求
  
  硬件方面你需要与SoundBlaster兼容的声卡,芯片最好使用奔腾III以上的芯片。内存最好不小于64MB。同时你需要安装下面的软件:
  
  ● Windows95/98,Windows NT 4.0, Windows2000或 WindowsXP。
  
  ●  JDK1.1.6或以上的Windows版本。
  
  ● JMF类和动态库
  
  在Windows下安装JMF2.1
  
  当下载了JMF2.1以后,运行jmf-2_1_1b-windows-i586.exe。该程序会将JMF2.1安装到你指定的目录下。当安装成功后,你需要确认一下安装程序正确设定了CLASSPATH和PATH环境变量。在CLASSPATH中需要包含jmf.jar和sound.jar;在PATH中需要包含JMF动态库的路径。
  JMFRegistry
  如果你希望使用视频和音频截取的设备,你需要确认安装了这些设备的驱动程序。除此之外,你还需要运行JMFRegistry应用程序。JMFRegistry可以向JMF注册新的数据源、媒体处理器、插件、视频和音频截取设备,然后你才能够在你的程序中使用它们。你只需要运行一次JMFRegistry就能注册系统中所有的视频和音频截取设备。
  当你运行了JMFRegistry后,会弹出图一所示的窗口:
   
  图一  通过JMFRegistry注册视频和音频截取设备
  选择“Capture Devices”标签,然后按下“Detect Capture  Devices”按钮,程序将自动检测出系统中的视频和音频截取设备。在左边的类表框中会列出所有检测到的设备的名称。在图一中我们看到JMFRegsitery发现了JavaSound  audio capture、vfw:Logitech USB Video Camera:0和vfw:Microsoft WDM Image Capture  (Win32):1。单击某个设备可以看到该设备支持的视频或音频格式。如果JMFRegistry无法检测到设备,有可能是没有正常安装设备的驱动程序。
  例子程序
  由于JMF2.1比较复杂,我不可能在在例子中包含JMF2.1支持的所有功能。因此我选择了下面几个在JMF中比较常用的功能:播放多媒体、注册音频和视频截取设备、截取视频和音频。
  1.播放多媒体
  在JMF.java中有一个play()方法。该方法可以播放用户选择的多媒体文件。当播放多媒体文件时,你需要一个Player对象。在例子中,dualPlayer就是Player接口的实现对象。
  Player  dualPlayer;
  在Play()方法中,通过使用FileDialog获得媒体文件的路径和文件名,并保存在filename中。
  
  try {
    FileDialog fd =
     new FileDialog(this, "Select  File", FileDialog.LOAD);
    fd.show();
    String filename =  fd.getDirectory() + fd.getFile();
    ...
   }
   catch (Exception e)  {
    System.out.println(e.toString());
   }
  然后你需要通过媒体管理器Manager间接创建一个Player对象。你可以使用Manager类的createPlayer()方法或者createProcessor()方法来获得一个Player对象或Processor对象。在play()方法中,我使用的是createPlayer()方法。
  dualPlayer = Manager.createPlayer
     (new MediaLocator("file:///" +  filename));
  有时你需要使用一个Player对象来控制多个其他的Player和Controller对象,我们把这个Player对象称为主对象,并把这些对象组成一个组。通过调用主对象中的start()、stop()、setMediaTime()等方法就可以激活组中所有成员的相应方法。主对象控制所有的状态变化和事件发布。然后使用addControllerListerner()方法来将一个ControllerListener对象绑定到Player对象上,Controller对象将向该ControllerListener对象发送事件消息。
  dualPlayer.addControllerListener(this);
  最后需要调用start()方法来启动Player对象。start()方法将Player对象的状态设置为Started。如果Player没有被实体化(Realize)或预取(Prefetch),start()方法会自动执行这些操作。
  dualPlayer.start();
  由于JMF类实现了ControllerLister接口,因此需要实现该接口中的controllerUpdate()方法,该方法在Controller对象产生一个事件时被调用。
  
  public synchronized void controllerUpdate(ControllerEvent event)  {
   if (event instanceof RealizeCompleteEvent) {
    Component  comp;
    if ((comp = dualPlayer.getVisualComponent()) != null)
     add  ("Center", comp);
    if ((comp = dualPlayer.getControlPanelComponent()) !=  null)
     add("South",  comp);
    validate();
   }
  }
  当JMF类产生了一个RealizeCompleteEvent事件后,controllerUpdate()方法在界面上增加两个Component对象,一个用于播放媒体,一个用于放置控制按钮,例如播放、停止等。
  在运行程序的过程中,程序会产生下面的输出。
  Starting player  ...javax.media.TransitionEvent
   [source=com.sun.media.content.video.mpeg.Handler@71bb78,
   previous=Unrealized,
   current=Realizing,
   target=Started]
  Open  log file:  C:/test/Java/JMF/JMF/jmf.log
  javax.media.DurationUpdateEvent
   [source=com.sun.media.content.video.mpeg.Handler@71bb78,duration=
   javax.media.Time@2a37a6
  javax.media.RealizeCompleteEvent
  [source=com.sun.media.content.video.mpeg.Handler@71bb78,
   previous=Realizing,
   current=Realized,
   target=Started]
  Adding  visual component
  Adding control  panel
  javax.media.TransitionEvent
   [source=com.sun.media.content.video.mpeg.Handler@71bb78,
   previous=Realized,
   current=Prefetching,
   target=Started]
  javax.media.PrefetchCompleteEvent
   [source=com.sun.media.content.video.mpeg.Handler@71bb78,
   previous=Prefetching,
   current=Prefetched,target=Started]
  javax.media.StartEvent
   [source=com.sun.media.content.video.mpeg.Handler@71bb78,
   previous=Prefetched,
   current=Started,
   target=Started,
   mediaTime=javax.media.Time@56a05e,timeBaseTime=
   javax.media.Time@3a8602]
  javax.media.EndOfMediaEvent
   [source=com.sun.media.content.video.mpeg.Handler@71bb78,
   previous=Started,
   current=Prefetched,
   target=Prefetched,
   mediaTime=javax.media.Time@1d332b]
  前面提到,当调用start()方法的时候,Player会切换到Started状态。从上面列出的信息中可以看到Player对象的状态从Unrealized变成了Started。当EndOfMedia事件被激活时(这时Player对象完成了媒体文件的播放),状态从Started变成了Prefetched。图二显示了程序正在播放多媒体文件时的情况。
   
  图二  程序正在播放媒体文件
  2.注册音频和视频截取设备
  在例子中,注册音频和视频截取设备的方法只在程序的内部注册这些设备,在程序外则不起作用。该方法的作用是当用户的计算机上存在多和音频和视频截取设备时,告诉程序因该使用哪个设备和这些设备支持的音频和视频格式。因此在进行截取处理之前需要获得设备的配置信息。在例子中,当在Configure菜单上按下Capture  Device命令后,会弹出CaptureDeviceDialog对话框。如果在截取音频或视频前没有设定设备的配置,也会弹出该对话框。图三显示了该对话框。
  
  图三  设备注册对话框
  让我们来看一下CaptureDeviceDialog类中的init()方法:在初始化了界面之后,通过调用CaptureDeviceManager类的getDeviceList()方法:
  
  devices = CaptureDeviceManager.getDeviceList ( null  );
  CaptureDeviceManager类使用查询机制和一个注册表来定位设备,然后将设备的信息放入CaptureDeviceInfo对象中返回。我们还可以利用CaptureDeviceManager类来注册新的设备。通过调用getDeviceList()方法程序获取了一个支持指定格式的设备的列表。在例子中,我将格式参数设定为null,这意味着设备可以使用任何格式。返回值被放入device变量中。如果getDeviceList()方法返回的是一个非空值,程序会将包含在其中的音频设备名称和视频设备名称分别放入两个下拉列表中中,但是到目前为止我们还不知道哪些设备是音频设备,哪些是视频设备。
  
  我们可以通过CaptureDeviceInfo的getFormat()方法获得Format对象组数,在Format对象中保存了设备支持的媒体格式。Format类间接被AudioFormat和VideoFormat类所继承。因此我们可以利用设备支持的格式类型来区分设备的类型:
  
  if (devices!=null && devices.size()>0) {
     int  deviceCount = devices.size();
     audioDevices = new  Vector();
     videoDevices = new Vector();
     Format[]  formats;
     for ( int i = 0; i < deviceCount; i++ ) {
      cdi =  (CaptureDeviceInfo) devices.elementAt ( i );
      formats =  cdi.getFormats();
      for ( int j=0; j<formats.length; j++ )  {
       if ( formats[j] instanceof AudioFormat )  {
        audioDevices.addElement(cdi);
        break;
       }
       else  if (formats[j] instanceof VideoFormat )  {
        videoDevices.addElement(cdi);
        break;
       }
      }
     }
     .  .  .
    }
  上面的程序运行后,audioDevices()中将包含所有的音频设备,videoDevices()中将保存所有的视频设备。其中cdi是CaptureDeviceInfo对象。然后将设备名称填入下拉列表中:
  
  // 将音频设备显示在下拉列表中
     for (int i=0; i<audioDevices.size();  i++) {
      cdi = (CaptureDeviceInfo)  audioDevices.elementAt(i);
      audioDeviceCombo.addItem(cdi.getName());
     }
     // 将视频设备显示在下拉列表中
     for (int i=0; i<videoDevices.size(); i++)  {
      cdi = (CaptureDeviceInfo)  videoDevices.elementAt(i);
      videoDeviceCombo.addItem(cdi.getName());
     }
  然后程序显示出当前选中的设备支持的格式:
  displayAudioFormats();
     displayVideoFormats();
  下一步需要获取用户选中的音频设备和视频设备以及它们支持的格式,相关的方法是JMF类中的getAudioDevice()、getVideoDevice()、getAudioFormat()和getVideoFormat()方法。然后将获取的对象分别保存到audioCDI,videoCDI,audioFormat和videoFormat中:
  
  audioCDI = cdDialog.getAudioDevice();
    if (audioCDI!=null)  {
     audioDeviceName =  audioCDI.getName();
     System.out.println("Audio Device Name: " +  audioDeviceName);
    }
    videoCDI =  cdDialog.getVideoDevice();
    if (videoCDI!=null) {
     videoDeviceName  = videoCDI.getName();
     System.out.println("Video Device Name: " +  videoDeviceName);
    }
    // 获得选中的多媒体格式
    videoFormat =  cdDialog.getVideoFormat();
    audioFormat =  cdDialog.getAudioFormat();
  3.截取视频和音频
  使用capture()方法可以截取音频和视频数据。但是在使用该方法前需要确定是否已经选中了视频和音频截取设备:
  if  (audioCDI==null && videoCDI==null)
     registerDevices();
  和play()方法类似,可以通过使用Manger类中的静态方法createPlayer()创建一个Player对象,该对象可以播放一个DataSource对象中的数据流。
  
  Player createPlayer(MediaLocator  sourceLocator)
  在例子中,我首先通过调用audioCDI和videoCDI的getLocator()方法来获得MediaLocator对象,然后利用Manager类的createPlayer()方法创建Player对象。最后将一个ControllerListener对象绑定到视频Player对象上并开始播放。
  
  videoPlayer =  Manager.createPlayer(videoCDI.getLocator());
      audioPlayer =  Manager.createPlayer(audioCDI.getLocator());
      videoPlayer.addControllerListener(this);
      videoPlayer.start();
      audioPlayer.start();
  使用这种方法导致最后获得了两个Player对象。我们也可以使用Manager类中的createDataSource()方法从视频和音频CaptureDeviceInfo对象(audioCID和videoCDI)中获得视频和音频数据源(DataSource对象),然后调用createMergingDataSource()方法将两个数据源合并成一个数据源(ds):
  
  DataSource[] dataSources = new  DataSource[2];
      dataSources[0]  =
       Manager.createDataSource(audioCDI.getLocator());
      dataSources[1]  =
       Manager.createDataSource(videoCDI.getLocator());
      DataSource  ds =  Manager.createMergingDataSource(dataSources);
  然后可以使用ds作为createPlayer()方法的参数来获得一个Player对象dualPlayer。调用addControllerListener()就可以进行播放了。
  
  dualPlayer =  Manager.createPlayer(ds);
  dualPlayer.addControllerListener(this);
  dualPlayer.start();
  小结
  Java多媒体框架是一个很好的多媒体编程工具。在这篇文章中我只是简单介绍了JMF的一些基本功能。如果有兴趣的话可以仔细阅读一下Sun公司的Java网站上提供的JMStudio的例子。在JMStudio中不仅实现了简单的播放和视频/音频截取功能,还实现了从互联网下载和向互联网上传多媒体数据流的功能。而且它还包含了JMFRegistry的源代码,将相应的代码移植到你的应用程序中后,你就不需要在运行程序前运行JMFRegistry来向JMF注册设备了

JMF框架的更多相关文章

  1. spring mvc官网下最新jar搭建框架-静态资源访问处理-注解-自动扫描

    1.从官网下载spring相关jar http://spring.io/projects 点击SPRING FRAMEWORK

  2. 从程序员到CTO的Java技术路线图 JAVA职业规划 JAVA职业发展路线图 系统后台框架图、前端工程师技能图 B2C电子商务基础系统架构解析

    http://zz563143188.iteye.com/blog/1877266在技术方面无论我们怎么学习,总感觉需要提升自已不知道自己处于什么水平了.但如果有清晰的指示图供参考还是非常不错的,这样 ...

  3. 避免重复造轮子的UI自动化测试框架开发

    一懒起来就好久没更新文章了,其实懒也还是因为忙,今年上半年的加班赶上了去年一年的加班,加班不息啊,好了吐槽完就写写一直打算继续的自动化开发 目前各种UI测试框架层出不穷,但是万变不离其宗,驱动PC浏览 ...

  4. ABP入门系列(1)——学习Abp框架之实操演练

    作为.Net工地搬砖长工一名,一直致力于挖坑(Bug)填坑(Debug),但技术却不见长进.也曾热情于新技术的学习,憧憬过成为技术大拿.从前端到后端,从bootstrap到javascript,从py ...

  5. 旺财速啃H5框架之Bootstrap(五)

    在上一篇<<旺财速啃H5框架之Bootstrap(四)>>做了基本的框架,<<旺财速啃H5框架之Bootstrap(二)>>篇里也大体认识了bootst ...

  6. Angular企业级开发(5)-项目框架搭建

    1.AngularJS Seed项目目录结构 AngularJS官方网站提供了一个angular-phonecat项目,另外一个就是Angular-Seed项目.所以大多数团队会基于Angular-S ...

  7. Scrapy框架爬虫初探——中关村在线手机参数数据爬取

    关于Scrapy如何安装部署的文章已经相当多了,但是网上实战的例子还不是很多,近来正好在学习该爬虫框架,就简单写了个Spider Demo来实践.作为硬件数码控,我选择了经常光顾的中关村在线的手机页面 ...

  8. 制作类似ThinkPHP框架中的PATHINFO模式功能

    一.PATHINFO功能简述 搞PHP的都知道ThinkPHP是一个免费开源的轻量级PHP框架,虽说轻量但它的功能却很强大.这也是我接触学习的第一个框架.TP框架中的URL默认模式即是PathInfo ...

  9. 旺财速啃H5框架之Bootstrap(四)

    上一篇<<旺财速啃H5框架之Bootstrap(三)>>已经把导航做了,接下来搭建内容框架.... 对于不规整的网页,要做成自适应就有点玩大了.... 例如下面这种版式的页面. ...

随机推荐

  1. 基于webapi的移动互联架构

    又到了一年最后一次上班了,写下这篇日志作为本年总结. 首先总体介绍一下项目背景,今年公司开发了一款app,本人一个人负责app的接口服务.微信开放平台搭建以及系统后台,上线半年,如今活跃用户数3W+. ...

  2. Gson解析纯Json数组

    [ { "type": "123", "value": 123 }, { "type": "234" ...

  3. angular $http 与form表单的select-->refine

    <!DOCTYPE html> <html ng-app="a2_15"> <head> <meta http-equiv="C ...

  4. Android selecter背景选择器使用

    android:drawable这个属性是必须的,默认时的背景图片. android:state_pressed布尔值.true指当用户点击或者触摸该控件的状态.默认为false android:st ...

  5. 将数据导入Excel

    /** * 查询未打印订单 * @param req * @param sort * @param order * @param rows * @param page * @return */ pub ...

  6. wget 扒站

    在Linux下,通过一个命令就可以把整个站相关的文件全部下载下来. wget -r -p -k -np [网址] 参数说明: -r : 递归下载 -p : 下载所有用于显示 HTML 页面的图片之类的 ...

  7. UIScrollView设置滑动的距离

    设置好scrollView.width即是控制滑动的距离, scrollView.clipsToBounds = NO;控制是否显示多出的部分(可灵活运用)

  8. .net中的反射(转载)

    原文地址:http://www.cnblogs.com/Stephenchao/p/4481995.html 两个现实中的例子:1.B超:大家体检的时候大概都做过B超吧,B超可以透过肚皮探测到你内脏的 ...

  9. [转]ubuntu 下无法启动chrome

    这很不爽,google了半天也不知道答案(搜索到要重装chrome,可是我怎么都卸载不干净.....),最终解决方法如下: -------------------------------------- ...

  10. JS根据经纬度获取地址信息

    <!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content ...