PageRenderTime 61ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/mp4seek/iso.py

https://github.com/Flumotion/mp4seek
Python | 1339 lines | 1010 code | 221 blank | 108 comment | 163 complexity | 8b82ddf06efb11ff2834f30169ec842d MD5 | raw file
  1. from math import ceil
  2. import struct
  3. import atoms
  4. from atoms import read_fcc, read_ulong, read_ulonglong
  5. def write_uchar(fobj, n):
  6. fobj.write(struct.pack('>B', n))
  7. def write_ulong(fobj, n):
  8. fobj.write(struct.pack('>L', n))
  9. def write_ulonglong(fobj, n):
  10. fobj.write(struct.pack('>Q', n))
  11. def write_fcc(fobj, fcc_str):
  12. # print '[wfcc]: @%d %r' % (fobj.tell(), fcc_str)
  13. fobj.write('%-4.4s' % fcc_str)
  14. def takeby(seq, n, force_tuples=False):
  15. if n == 1 and not force_tuples:
  16. return seq
  17. return [tuple(seq[i:i + n]) for i in xrange(0, len(seq), n)]
  18. def read_table(f, row_spec, entries, spec_prefix='>'):
  19. """Read a continuous region of file and unpack it into a list of
  20. tuples using the given struct specification of a single row.
  21. @param row_spec: spec describing single row of table using same
  22. syntax as in L{struct} module.
  23. @type row_spec: str
  24. @param entries: number of rows to read
  25. @type entries: int
  26. @param spec_prefix: optional specification that will be used for
  27. the whole table
  28. @type spec_prefix: str
  29. """
  30. if entries == 0:
  31. return []
  32. row_bytes = struct.calcsize('%s%s' % (spec_prefix, row_spec))
  33. data = f.read(row_bytes * entries)
  34. try:
  35. l = struct.unpack('%s%s' % (spec_prefix, row_spec * entries), data)
  36. except struct.error:
  37. raise RuntimeError('Not enough data: requested %d, read %d' %
  38. (row_bytes * entries, len(data)))
  39. per_row = len(l) / entries
  40. return takeby(l, per_row)
  41. class UnsuportedVersion(Exception):
  42. pass
  43. class FormatError(Exception):
  44. pass
  45. class CannotSelect(Exception):
  46. pass
  47. class AttribInitializer(type):
  48. def __new__(meta, classname, bases, classdict):
  49. if '_fields' in classdict:
  50. fields = classdict['_fields']
  51. orig_init = classdict.pop('__init__', None)
  52. def __init__(self, *a, **kw):
  53. f_dict = {}
  54. for f in fields:
  55. f_dict[f] = kw.pop(f, None)
  56. if orig_init:
  57. self.__dict__.update(f_dict)
  58. orig_init(self, *a, **kw)
  59. elif bases and bases[0] != object:
  60. super(self.__class__, self).__init__(*a, **kw)
  61. self.__dict__.update(f_dict)
  62. classdict['__init__'] = __init__
  63. if '__repr__' not in classdict:
  64. def __repr__(self):
  65. r = '%s(%s)' % (self.__class__.__name__,
  66. ', '.join(['%s=%r' % (n, getattr(self, n))
  67. for n in fields]))
  68. return r
  69. classdict['__repr__'] = __repr__
  70. return type.__new__(meta, classname, bases, classdict)
  71. class Box(object):
  72. __metaclass__ = AttribInitializer
  73. def __init__(self, atom):
  74. self._atom = atom
  75. def get_size(self):
  76. # should be overriden in the boxes we want to be able to modify
  77. return self._atom.get_size()
  78. def get_offset(self):
  79. return self._atom.get_offset()
  80. def copy(self, *a, **kw):
  81. cls = self.__class__
  82. if getattr(self, '_fields', None):
  83. attribs = dict([(k, getattr(self, k)) for k in self._fields])
  84. attribs.update(dict([(k, kw[k]) for k in self._fields if k in kw]))
  85. else:
  86. attribs = {}
  87. return cls(self._atom, **attribs)
  88. def write(self, fobj):
  89. # print '[ b] writing:', self
  90. self._atom.write(fobj)
  91. def write_head(self, fobj):
  92. # assuming 'short' sizes for now - FIXME!
  93. # print '[ b] writing head:', self._atom
  94. a = self._atom
  95. write_ulong(fobj, self.get_size())
  96. write_fcc(fobj, a.type)
  97. if (a.extended_type):
  98. fobj.write(a.extended_type)
  99. class FullBox(Box):
  100. def tabled_size(self, body_size, loop_size):
  101. # TODO: move to a separate TableFullBox subclass?
  102. return (self._atom.head_size_ext() + body_size +
  103. len(self.table) * loop_size)
  104. def write_head(self, fobj):
  105. Box.write_head(self, fobj)
  106. a = self._atom
  107. write_ulong(fobj, (a.v & 0xff) << 24 | (a.flags & 0xffffff))
  108. class ContainerBox(Box):
  109. def __init__(self, *a, **kw):
  110. Box.__init__(self, *a, **kw)
  111. self._extra_children = []
  112. def get_size(self):
  113. # print '[>] getting size: %r' % self._atom
  114. fields = getattr(self, '_fields', [])
  115. cd = self._atom.get_children_dict()
  116. size = self._atom.head_size_ext()
  117. for k, v in cd.items():
  118. if k in fields:
  119. v = getattr(self, k)
  120. if not isinstance(v, (tuple, list)):
  121. if v is None:
  122. v = []
  123. else:
  124. v = [v]
  125. # print 'size for %r = %r' % (sum([a.get_size() for a in v]), v)
  126. size += sum([a.get_size() for a in v])
  127. size += sum([a.get_size() for a in self._extra_children])
  128. # print '[<] getting size: %r = %r' % (self._atom, size)
  129. return size
  130. def write(self, fobj):
  131. self.write_head(fobj)
  132. fields = getattr(self, '_fields', [])
  133. cd = self._atom.get_children_dict()
  134. to_write = []
  135. for k, v in cd.items():
  136. if k in fields:
  137. v = getattr(self, k)
  138. if not isinstance(v, (tuple, list)):
  139. if v is None:
  140. v = []
  141. else:
  142. v = [v]
  143. to_write.extend(v)
  144. def _get_offset(a):
  145. return a.get_offset()
  146. to_write.sort(key=_get_offset)
  147. to_write.extend(self._extra_children)
  148. # print '[ ] going to write:', \
  149. # ([(isinstance(a, Box) and a._atom.type or a.type)
  150. # for a in to_write])
  151. for ca in to_write:
  152. # print '[cb] writing:', ca
  153. ca.write(fobj)
  154. def add_extra_children(self, al):
  155. self._extra_children.extend(al)
  156. def fullboxread(f):
  157. def _with_full_atom_read_wrapper(cls, a):
  158. return f(cls, atoms.full(a))
  159. return _with_full_atom_read_wrapper
  160. def containerboxread(f):
  161. def _with_container_atom_read_wrapper(cls, a):
  162. return f(cls, atoms.container(a))
  163. return _with_container_atom_read_wrapper
  164. def ver_skip(atom, sizes):
  165. if atom.v > len(sizes) or atom.v < 0:
  166. raise UnsuportedVersion('version requested: %d' % atom.v)
  167. atom.skip(sizes[atom.v])
  168. def ver_read(atom, readers):
  169. if atom.v > len(readers) or atom.v < 0:
  170. raise UnsuportedVersion('version requested: %d' % atom.v)
  171. return readers[atom.v](atom.f)
  172. def maybe_build_atoms(atype, alist):
  173. cls = globals().get(atype)
  174. if cls and issubclass(cls, Box):
  175. return map(cls.read, alist)
  176. return alist
  177. def select_children_atoms(a, *selection):
  178. return select_atoms(a.get_children_dict(), *selection)
  179. def select_atoms(ad, *selection):
  180. """ad: atom dict
  181. selection: [(type, min_required, max_required), ...]"""
  182. selected = []
  183. for atype, req_min, req_max in selection:
  184. alist = ad.get(atype, [])
  185. found = len(alist)
  186. if ((req_min is not None and found < req_min) or
  187. (req_max is not None and found > req_max)):
  188. raise CannotSelect('requested number of atoms %r: in [%s; %s],'
  189. ' found: %d (all children: %r)' %
  190. (atype, req_min, req_max, found, ad))
  191. alist = maybe_build_atoms(atype, alist)
  192. if req_max == 1:
  193. if found == 0:
  194. selected.append(None)
  195. else:
  196. selected.append(alist[0])
  197. else:
  198. selected.append(alist)
  199. return selected
  200. def find_atom(alist, type):
  201. return [a.type for a in alist].index(type)
  202. def write_atoms(alist, f):
  203. # alist - list of Atoms or Boxes
  204. for a in alist:
  205. a.write(f)
  206. def find_samplenum_stts(stts, mt):
  207. "stts - table of the 'stts' atom; mt - media time"
  208. ctime = 0
  209. samples = 1
  210. i, n = 0, len(stts)
  211. while i < n:
  212. # print 'fsstts:', mt, ctime, stts[i], samples, ctime
  213. if mt == ctime:
  214. break
  215. count, delta = stts[i]
  216. cdelta = count * delta
  217. if mt < ctime + cdelta:
  218. samples += int(ceil((mt - ctime) / float(delta)))
  219. break
  220. ctime += cdelta
  221. samples += count
  222. i += 1
  223. return samples
  224. def find_mediatime_stts(stts, sample):
  225. ctime = 0
  226. samples = 1
  227. i, n = 0, len(stts)
  228. while i < n:
  229. count, delta = stts[i]
  230. if samples + count >= sample:
  231. return ctime + (sample - samples) * delta
  232. ctime += count * delta
  233. samples += count
  234. i += 1
  235. return ctime
  236. def find_mediatimes(stts, samples):
  237. ctime = 0
  238. total_samples = 1
  239. ret = []
  240. i, n = 0, len(stts)
  241. j, m = 0, len(samples)
  242. while i < n and j < m:
  243. count, delta = stts[i]
  244. sample = samples[j]
  245. if total_samples + count >= sample:
  246. ret.append(ctime + (sample - total_samples) * delta)
  247. j += 1
  248. continue
  249. ctime += count * delta
  250. total_samples += count
  251. i += 1
  252. return ret
  253. def find_chunknum_stsc(stsc, sample_num):
  254. current = 1 # 1-based indices!
  255. per_chunk = 0
  256. samples = 1
  257. i, n = 0, len(stsc)
  258. while i < n:
  259. # print 'fcnstsc:', sample_num, current, stsc[i], samples, per_chunk
  260. next, next_per_chunk, _sdidx = stsc[i]
  261. samples_here = (next - current) * per_chunk
  262. if samples + samples_here > sample_num:
  263. break
  264. samples += samples_here
  265. current, per_chunk = next, next_per_chunk
  266. i += 1
  267. return int((sample_num - samples) // per_chunk + current)
  268. def get_chunk_offset(stco64, chunk_num):
  269. # 1-based indices!
  270. return stco64[chunk_num - 1]
  271. class uuid(FullBox):
  272. _extended_type = None
  273. @classmethod
  274. def read(cls, a):
  275. # TODO implement a lookup of child classes based on _extended_type?
  276. raise Exception("not implemented yet")
  277. class uuid_sscurrent(uuid):
  278. _fields = ('timestamp', 'duration')
  279. _extended_type = "\x6d\x1d\x9b\x05\x42\xd5\x44\xe6\x80\xe2\x14\x1d\xaf\xf7\x57\xb2"
  280. def write(self, fobj):
  281. self.write_head(fobj)
  282. write_ulonglong(fobj, self.timestamp)
  283. write_ulonglong(fobj, self.duration)
  284. def get_size(self):
  285. size = self._atom.head_size_ext()
  286. size += 2 * 8
  287. return size
  288. @classmethod
  289. def read(cls, a):
  290. raise Exception("not implemented yet")
  291. @classmethod
  292. def make(cls, timestamp, duration):
  293. a = atoms.FullAtom(0, "uuid", 0, 1, 0, None, extended_type=cls._extended_type)
  294. s = cls(a)
  295. s.timestamp = timestamp
  296. s.duration = duration
  297. return s
  298. class uuid_ssnext(FullBox):
  299. _fields = ('entries')
  300. _extended_type = "\xd4\x80\x7e\xf2\xca\x39\x46\x95\x8e\x54\x26\xcb\x9e\x46\xa7\x9f"
  301. def write(self, fobj):
  302. self.write_head(fobj)
  303. write_uchar(fobj, len(self.entries))
  304. for ts, duration in self.entries:
  305. write_ulonglong(fobj, ts)
  306. write_ulonglong(fobj, duration)
  307. def get_size(self):
  308. size = self._atom.head_size_ext()
  309. size += 1 + (2 * 8) * len(self.entries)
  310. return size
  311. @classmethod
  312. def read(cls, a):
  313. raise Exception("not implemented yet")
  314. @classmethod
  315. def make(cls, entries):
  316. a = atoms.FullAtom(0, "uuid", 0, 1, 0, None, extended_type=cls._extended_type)
  317. s = cls(a)
  318. s.entries = entries
  319. return s
  320. class mvhd(FullBox):
  321. _fields = (
  322. # 'creation_time', 'modification_time',
  323. 'timescale', 'duration',
  324. # 'rate', 'volume', 'matrix', 'next_track_ID'
  325. )
  326. @classmethod
  327. @fullboxread
  328. def read(cls, a):
  329. ver_skip(a, (8, 16))
  330. ts = read_ulong(a.f)
  331. d = ver_read(a, (read_ulong, read_ulonglong))
  332. return cls(a, timescale=ts, duration=d)
  333. def write(self, fobj):
  334. self.write_head(fobj)
  335. a = self._atom
  336. a.seek_to_start()
  337. a.skip(a.head_size_ext())
  338. if a.v == 0:
  339. fobj.write(a.read_bytes(8))
  340. write_ulong(fobj, self.timescale)
  341. write_ulong(fobj, self.duration)
  342. a.skip(8)
  343. elif a.v == 1:
  344. fobj.write(a.read_bytes(16))
  345. write_ulong(fobj, self.timescale)
  346. write_ulonglong(fobj, self.duration)
  347. a.skip(12)
  348. else:
  349. raise RuntimeError()
  350. fobj.write(a.read_bytes(80))
  351. class tkhd(FullBox):
  352. _fields = ('duration', 'id')
  353. @classmethod
  354. @fullboxread
  355. def read(cls, a):
  356. ver_skip(a, (8, 16))
  357. id = read_ulong(a.f)
  358. a.skip(4)
  359. d = ver_read(a, (read_ulong, read_ulonglong))
  360. return cls(a, duration=d, id=id)
  361. def write(self, fobj):
  362. self.write_head(fobj)
  363. a = self._atom
  364. a.seek_to_start()
  365. a.skip(a.head_size_ext())
  366. if a.v == 0:
  367. fobj.write(a.read_bytes(8))
  368. write_ulong(fobj, self.id)
  369. fobj.write(a.read_bytes(4))
  370. write_ulong(fobj, self.duration)
  371. a.skip(4)
  372. elif a.v == 1:
  373. fobj.write(a.read_bytes(16))
  374. write_ulong(fobj, self.id)
  375. fobj.write(a.read_bytes(4))
  376. write_ulonglong(fobj, self.duration)
  377. a.skip(8)
  378. else:
  379. raise RuntimeError()
  380. fobj.write(a.read_bytes(60))
  381. class mdhd(FullBox):
  382. _fields = ('timescale', 'duration')
  383. @classmethod
  384. @fullboxread
  385. def read(cls, a):
  386. ver_skip(a, (8, 16))
  387. ts = read_ulong(a.f)
  388. d = ver_read(a, (read_ulong, read_ulonglong))
  389. return cls(a, timescale=ts, duration=d)
  390. def write(self, fobj):
  391. self.write_head(fobj)
  392. a = self._atom
  393. a.seek_to_start()
  394. a.skip(a.head_size_ext())
  395. if a.v == 0:
  396. fobj.write(a.read_bytes(8))
  397. write_ulong(fobj, self.timescale)
  398. write_ulong(fobj, self.duration)
  399. a.skip(8)
  400. elif a.v == 1:
  401. fobj.write(a.read_bytes(16))
  402. write_ulong(fobj, self.timescale)
  403. write_ulonglong(fobj, self.duration)
  404. a.skip(12)
  405. else:
  406. raise RuntimeError()
  407. fobj.write(a.read_bytes(4))
  408. class stts(FullBox):
  409. _fields = ('table',)
  410. @classmethod
  411. @fullboxread
  412. def read(cls, a):
  413. entries = read_ulong(a.f)
  414. t = read_table(a.f, 'LL', entries)
  415. return cls(a, table=t)
  416. def get_size(self):
  417. return self.tabled_size(4, 8)
  418. def write(self, fobj):
  419. self.write_head(fobj)
  420. write_ulong(fobj, len(self.table))
  421. for elt in self.table:
  422. write_ulong(fobj, elt[0])
  423. write_ulong(fobj, elt[1])
  424. class ctts(FullBox):
  425. _fields = ('table',)
  426. @classmethod
  427. @fullboxread
  428. def read(cls, a):
  429. entries = read_ulong(a.f)
  430. t = read_table(a.f, 'LL', entries)
  431. return cls(a, table=t)
  432. def get_size(self):
  433. return self.tabled_size(4, 8)
  434. def write(self, fobj):
  435. self.write_head(fobj)
  436. write_ulong(fobj, len(self.table))
  437. for elt in self.table:
  438. write_ulong(fobj, elt[0])
  439. write_ulong(fobj, elt[1])
  440. class stss(FullBox):
  441. _fields = ('table',)
  442. @classmethod
  443. @fullboxread
  444. def read(cls, a):
  445. entries = read_ulong(a.f)
  446. t = read_table(a.f, 'L', entries)
  447. return cls(a, table=t)
  448. def get_size(self):
  449. return self.tabled_size(4, 4)
  450. def write(self, fobj):
  451. self.write_head(fobj)
  452. write_ulong(fobj, len(self.table))
  453. for elt in self.table:
  454. write_ulong(fobj, elt)
  455. class stsz(FullBox):
  456. _fields = ('sample_size', 'table')
  457. @classmethod
  458. @fullboxread
  459. def read(cls, a):
  460. ss = read_ulong(a.f)
  461. entries = read_ulong(a.f)
  462. if ss == 0:
  463. t = read_table(a.f, 'L', entries)
  464. else:
  465. t = []
  466. return cls(a, sample_size=ss, table=t)
  467. def get_size(self):
  468. if self.sample_size != 0:
  469. return self._atom.head_size_ext() + 8
  470. return self.tabled_size(8, 4)
  471. def write(self, fobj):
  472. self.write_head(fobj)
  473. write_ulong(fobj, self.sample_size)
  474. write_ulong(fobj, len(self.table))
  475. if self.sample_size == 0:
  476. for elt in self.table:
  477. write_ulong(fobj, elt)
  478. class stsc(FullBox):
  479. _fields = ('table',)
  480. @classmethod
  481. @fullboxread
  482. def read(cls, a):
  483. entries = read_ulong(a.f)
  484. t = read_table(a.f, 'LLL', entries)
  485. return cls(a, table=t)
  486. def get_size(self):
  487. return self.tabled_size(4, 12)
  488. def write(self, fobj):
  489. self.write_head(fobj)
  490. write_ulong(fobj, len(self.table))
  491. for elt in self.table:
  492. write_ulong(fobj, elt[0])
  493. write_ulong(fobj, elt[1])
  494. write_ulong(fobj, elt[2])
  495. class stco(FullBox):
  496. _fields = ('table',)
  497. @classmethod
  498. @fullboxread
  499. def read(cls, a):
  500. entries = read_ulong(a.f)
  501. t = read_table(a.f, 'L', entries)
  502. return cls(a, table=t)
  503. def get_size(self):
  504. return self.tabled_size(4, 4)
  505. def write(self, fobj):
  506. self.write_head(fobj)
  507. write_ulong(fobj, len(self.table))
  508. for elt in self.table:
  509. write_ulong(fobj, elt)
  510. class co64(FullBox):
  511. _fields = ('table',)
  512. @classmethod
  513. @fullboxread
  514. def read(cls, a):
  515. entries = read_ulong(a.f)
  516. t = read_table(a.f, 'Q', entries)
  517. return cls(a, table=t)
  518. def get_size(self):
  519. return self.tabled_size(4, 8)
  520. def write(self, fobj):
  521. self.write_head(fobj)
  522. write_ulong(fobj, len(self.table))
  523. for elt in self.table:
  524. write_ulonglong(fobj, elt)
  525. class stz2(FullBox):
  526. _fields = ('field_size', 'table')
  527. @classmethod
  528. @fullboxread
  529. def read(cls, a):
  530. field_size = read_ulong(a.f) & 0xff
  531. entries = read_ulong(a.f)
  532. def read_2u4(f):
  533. b = read_bytes(f, 1)
  534. return (b >> 4) & 0x0f, b & 0x0f
  535. def flatten(l):
  536. ret = []
  537. for elt in l:
  538. ret.extend(elt)
  539. return ret
  540. if field_size == 16:
  541. t = read_table(a.f, 'H', entries)
  542. elif field_size == 8:
  543. t = read_table(a.f, 'B', entries)
  544. elif field_size == 4:
  545. t = flatten([read_2u4(a.f) for _ in xrange((entries + 1) / 2)])
  546. else:
  547. raise FormatError()
  548. return cls(a, field_size=field_size, table=t)
  549. def get_size(self):
  550. fs = self.field_size / 8.0
  551. return int(self.tabled_size(8, fs))
  552. def write(self, fobj):
  553. self.write_head(fobj)
  554. write_ulong(fobj, self.field_size & 0xff)
  555. write_ulong(fobj, len(self.table))
  556. def write_u16(f, n):
  557. fobj.write(struct.pack('>H', n))
  558. def write_u8(f, n):
  559. fobj.write(struct.pack('B', n))
  560. def write_2u4(f, n, m):
  561. fobj.write(struct.pack('B', ((n & 0x0f) << 4) | (m & 0x0f)))
  562. if field_size == 16:
  563. for elt in self.table:
  564. write_u16(fobj, elt)
  565. elif field_size == 8:
  566. for elt in self.table:
  567. write_u8(fobj, elt)
  568. elif field_size == 4:
  569. for elt in takeby(self.table, 2):
  570. write_2u4(fobj, *elt)
  571. else:
  572. raise FormatError()
  573. class btrt(Box):
  574. _fields = ('bufferSize', 'maxBitrate', 'avgBitrate')
  575. @classmethod
  576. def read(cls, a):
  577. a.seek_to_data()
  578. bufferSize = atoms.read_ulong(a.f)
  579. maxBitrate = atoms.read_ulong(a.f)
  580. avgBitrate = atoms.read_ulong(a.f)
  581. return cls(a, bufferSize=bufferSize, maxBitrate=maxBitrate,
  582. avgBitrate=avgBitrate)
  583. # from gst and mp4split, which all seem to be from ffmpeg
  584. def read_desc_len(f):
  585. bytes = 0
  586. len = 0
  587. while True:
  588. c = atoms.read_uchar(f)
  589. len <<= 7
  590. len |= c & 0x7f
  591. bytes += 1
  592. if (bytes == 4):
  593. break
  594. if not (c & 0x80):
  595. break
  596. return len
  597. MP4_ELEMENTARY_STREAM_DESCRIPTOR_TAG = 3
  598. MP4_DECODER_CONFIG_DESCRIPTOR_TAG = 4
  599. MP4_DECODER_SPECIFIC_DESCRIPTOR_TAG = 5
  600. class esds(FullBox):
  601. _fields = ('object_type_id', 'maxBitrate', 'avgBitrate', 'data')
  602. @classmethod
  603. @fullboxread
  604. def read(cls, a):
  605. # from mp4split
  606. esdesc = atoms.read_uchar(a.f)
  607. if esdesc == MP4_ELEMENTARY_STREAM_DESCRIPTOR_TAG:
  608. len = read_desc_len(a.f)
  609. stream_id = atoms.read_ushort(a.f)
  610. prio = atoms.read_uchar(a.f)
  611. else:
  612. stream_id = atoms.read_ushort(a.f)
  613. tag = atoms.read_uchar(a.f)
  614. len = read_desc_len(a.f)
  615. if tag != MP4_DECODER_CONFIG_DESCRIPTOR_TAG:
  616. raise FormatError("can't parse esds")
  617. object_type_id = atoms.read_uchar(a.f)
  618. stream_type = atoms.read_uchar(a.f)
  619. buffer_size_db = a.read_bytes(3)
  620. maxBitrate = atoms.read_ulong(a.f)
  621. avgBitrate = atoms.read_ulong(a.f)
  622. tag = atoms.read_uchar(a.f)
  623. len = read_desc_len(a.f)
  624. if tag != MP4_DECODER_SPECIFIC_DESCRIPTOR_TAG:
  625. raise FormatError("can't parse esd")
  626. data = a.read_bytes(len)
  627. return cls(a, object_type_id=object_type_id,
  628. maxBitrate=maxBitrate, avgBitrate=avgBitrate, data=data)
  629. class mp4a(Box):
  630. # TODO: base class for SampleEntry, AudioSampleEntry...
  631. _fields = ('index', 'channelcount', 'samplesize', 'sampleratehi', 'sampleratelo', 'extra')
  632. @classmethod
  633. def read(cls, a):
  634. a.seek_to_data()
  635. a.skip(6) # reserved
  636. idx = atoms.read_ushort(a.f)
  637. version = atoms.read_ushort(a.f)
  638. a.skip(4 + 2) # reserved
  639. channelcount = atoms.read_ushort(a.f)
  640. if channelcount == 3:
  641. channelcount = 6 # from mp4split
  642. samplesize = atoms.read_ushort(a.f)
  643. a.skip(4)
  644. sampleratehi = atoms.read_ushort(a.f)
  645. sampleratelo = atoms.read_ushort(a.f)
  646. # FIXME: parse version != 0 samples_per_packet etc..
  647. # optional boxes follow
  648. extra = list(atoms.read_atoms(a.f, a.size - 36))
  649. a.seek_to_data()
  650. a.skip(36)
  651. extra = map(lambda a: maybe_build_atoms(a.type, [a])[0], extra)
  652. return cls(a, index=idx, channelcount=channelcount, samplesize=samplesize,
  653. sampleratehi=sampleratehi, sampleratelo=sampleratelo,
  654. extra=extra)
  655. class avcC(Box):
  656. _fields = ('version', 'profile', 'level', 'data')
  657. @classmethod
  658. def read(cls, a):
  659. a.seek_to_data()
  660. data = a.read_bytes(a.size - 8)
  661. version = data[0]
  662. profile = data[1]
  663. level = data[3]
  664. return cls(a, version=version, profile=profile, level=level, data=data)
  665. class avc1(Box):
  666. # TODO: base class for SampleEntry, VideoSampleEntry...
  667. _fields = ('index', 'width', 'height', 'comp', 'extra')
  668. @classmethod
  669. def read(cls, a):
  670. a.seek_to_data()
  671. a.skip(6)
  672. idx = atoms.read_ushort(a.f)
  673. a.skip(4 * 4)
  674. width = atoms.read_ushort(a.f)
  675. height = atoms.read_ushort(a.f)
  676. hr = a.read_bytes(4)
  677. vr = a.read_bytes(4)
  678. reserved = atoms.read_ulong(a.f)
  679. fc = atoms.read_ushort(a.f)
  680. comp = a.read_bytes(32)
  681. depth = atoms.read_ushort(a.f)
  682. minusone = atoms.read_short(a.f)
  683. if (minusone != -1):
  684. raise FormatError()
  685. # optional boxes follow
  686. extra = list(atoms.read_atoms(a.f, a.size - 86))
  687. a.seek_to_data()
  688. a.skip(86)
  689. extra = map(lambda a: maybe_build_atoms(a.type, [a])[0], extra)
  690. return cls(a, index=idx, width=width, height=height, comp=comp, extra=extra)
  691. class stsd(FullBox):
  692. _fields = ('count','entries')
  693. @classmethod
  694. @fullboxread
  695. def read(cls, a):
  696. count = read_ulong(a.f)
  697. entries = []
  698. while count > 0:
  699. b = atoms.read_atom(a.f)
  700. entries.append(b)
  701. count = count - 1
  702. entries = map(lambda a: maybe_build_atoms(a.type, [a])[0], entries)
  703. return cls(a, count=count, entries=entries)
  704. class stbl(ContainerBox):
  705. _fields = ('stss', 'stsz', 'stz2', 'stco', 'co64', 'stts', 'ctts', 'stsc', 'stsd')
  706. @classmethod
  707. @containerboxread
  708. def read(cls, a):
  709. (astss, astsz, astz2, astco, aco64, astts, actts, astsc, stsd) = \
  710. select_children_atoms(a, ('stss', 0, 1), ('stsz', 0, 1),
  711. ('stz2', 0, 1), ('stco', 0, 1),
  712. ('co64', 0, 1), ('stts', 1, 1),
  713. ('ctts', 0, 1), ('stsc', 1, 1),
  714. ('stsd', 0, 1))
  715. return cls(a, stss=astss, stsz=astsz, stz2=astz2, stco=astco,
  716. co64=aco64, stts=astts, ctts=actts, stsc=astsc,
  717. stsd=stsd)
  718. class minf(ContainerBox):
  719. _fields = ('stbl',)
  720. @classmethod
  721. @containerboxread
  722. def read(cls, a):
  723. (astbl,) = select_children_atoms(a, ('stbl', 1, 1))
  724. return cls(a, stbl=astbl)
  725. class mdia(ContainerBox):
  726. _fields = ('mdhd', 'minf')
  727. @classmethod
  728. @containerboxread
  729. def read(cls, a):
  730. (amdhd, aminf) = select_children_atoms(a, ('mdhd', 1, 1),
  731. ('minf', 1, 1))
  732. return cls(a, mdhd=amdhd, minf=aminf)
  733. class trak(ContainerBox):
  734. _fields = ('tkhd', 'mdia')
  735. @classmethod
  736. @containerboxread
  737. def read(cls, a):
  738. (atkhd, amdia) = select_children_atoms(a, ('tkhd', 1, 1),
  739. ('mdia', 1, 1))
  740. return cls(a, tkhd=atkhd, mdia=amdia)
  741. class moov(ContainerBox):
  742. _fields = ('mvhd', 'trak')
  743. @classmethod
  744. @containerboxread
  745. def read(cls, a):
  746. (amvhd, traks) = select_children_atoms(a, ('mvhd', 1, 1),
  747. ('trak', 1, None))
  748. return cls(a, mvhd=amvhd, trak=traks)
  749. class ftyp(Box):
  750. _fields = ('brand', 'version')
  751. @classmethod
  752. def read(cls, a):
  753. a.seek_to_data()
  754. brand = read_fcc(a.f)
  755. v = read_ulong(a.f)
  756. return cls(a, brand=brand, version=v)
  757. class tfhd(FullBox):
  758. _fields = ('track_id', )
  759. @classmethod
  760. @fullboxread
  761. def read(cls, a):
  762. track_id = read_ulong(a.f)
  763. return cls(a, track_id=track_id)
  764. class traf(ContainerBox):
  765. _fields = ('tfhd', 'trun', 'sdtp', 'uuid')
  766. @classmethod
  767. @containerboxread
  768. def read(cls, a):
  769. (tfhd, trun, sdtp) = select_children_atoms(a, ('tfhd', 1, 1),
  770. ('trun', 1, 1),
  771. ('sdtp', 0, 1))
  772. uuid = []
  773. return cls(a, tfhd=tfhd, trun=trun, sdtp=sdtp, uuid=uuid)
  774. class moof(ContainerBox):
  775. _fields = ('mfhd', 'traf')
  776. @classmethod
  777. @containerboxread
  778. def read(cls, a):
  779. (mfhd, traf) = select_children_atoms(a, ('mfhd', 1, 1),
  780. ('traf', 1, 1))
  781. return cls(a, mfhd=mfhd, traf=traf)
  782. def read_iso_file(fobj):
  783. fobj.seek(0)
  784. al = list(atoms.read_atoms(fobj))
  785. ad = atoms.atoms_dict(al)
  786. aftyp, amoov, mdat = select_atoms(ad, ('ftyp', 1, 1), ('moov', 1, 1),
  787. ('mdat', 1, None))
  788. # print '(first mdat offset: %d)' % mdat[0].offset
  789. return aftyp, amoov, al
  790. def find_cut_trak_info(atrak, t):
  791. ts = atrak.mdia.mdhd.timescale
  792. stbl = atrak.mdia.minf.stbl
  793. mt = int(round(t * ts))
  794. # print 'media time:', mt, t, ts, t * ts
  795. # print ('finding cut for trak %r @ time %r (%r/%r)' %
  796. # (atrak._atom, t, mt, ts))
  797. sample = find_samplenum_stts(stbl.stts.table, mt)
  798. chunk = find_chunknum_stsc(stbl.stsc.table, sample)
  799. # print ('found sample: %d and chunk: %d/%r' %
  800. # (sample, chunk, stbl.stsc.table[-1]))
  801. stco64 = stbl.stco or stbl.co64
  802. chunk_offset = get_chunk_offset(stco64.table, chunk)
  803. zero_offset = get_chunk_offset(stco64.table, 1)
  804. # print 'found chunk offsets:', chunk_offset, zero_offset
  805. return sample, chunk, zero_offset, chunk_offset
  806. def cut_stco64(stco64, chunk_num, offset_change, first_chunk_delta=0):
  807. new_table = [offset - offset_change for offset in stco64[chunk_num - 1:]]
  808. if new_table and first_chunk_delta:
  809. new_table[0] = new_table[0] + first_chunk_delta
  810. return new_table
  811. def cut_stco64_stsc(stco64, stsc, stsz2, chunk_num, sample_num, offset_change):
  812. new_stsc = None
  813. i, n = 0, len(stsc)
  814. current, per_chunk, sdidx = 1, 0, None
  815. samples = 1
  816. while i < n:
  817. next, next_per_chunk, next_sdidx = stsc[i]
  818. if next > chunk_num:
  819. offset = chunk_num - 1
  820. new_stsc = ([(1, per_chunk, sdidx)]
  821. + [(c - offset, p_c, j)
  822. for (c, p_c, j) in stsc[i:]])
  823. break
  824. samples += (next - current) * per_chunk
  825. current, per_chunk, sdidx = next, next_per_chunk, next_sdidx
  826. i += 1
  827. if new_stsc is None:
  828. new_stsc = [(1, per_chunk, sdidx)]
  829. lead_samples = (sample_num - samples) % per_chunk
  830. bytes_offset = 0
  831. if lead_samples > 0:
  832. bytes_offset = sum(stsz2[sample_num - 1 - lead_samples :
  833. sample_num - 1])
  834. # print 'lead_samples:', lead_samples, 'bytes_offset:', bytes_offset
  835. if lead_samples > 0:
  836. fstsc = new_stsc[0]
  837. new_fstsc = (1, fstsc[1] - lead_samples, fstsc[2])
  838. # print 'old stsc', new_stsc
  839. if len(new_stsc) > 1 and new_stsc[1][0] == 2:
  840. new_stsc[0] = new_fstsc
  841. else:
  842. new_stsc[0:1] = [new_fstsc, (2, fstsc[1], fstsc[2])]
  843. # print 'new stsc', new_stsc
  844. return (cut_stco64(stco64, chunk_num, offset_change, bytes_offset),
  845. new_stsc)
  846. def cut_sctts(sctts, sample):
  847. samples = 1
  848. i, n = 0, len(sctts)
  849. while i < n:
  850. count, delta = sctts[i]
  851. if samples + count > sample:
  852. return [(samples + count - sample, delta)] + sctts[i+1:]
  853. samples += count
  854. i += 1
  855. return [] # ? :/
  856. def cut_stss(stss, sample):
  857. i, n = 0, len(stss)
  858. while i < n:
  859. snum = stss[i]
  860. # print 'cut_stss:', snum, sample
  861. if snum >= sample:
  862. return [s - sample + 1 for s in stss[i:]]
  863. i += 1
  864. return []
  865. def cut_stsz2(stsz2, sample):
  866. if not stsz2:
  867. return []
  868. return stsz2[sample - 1:]
  869. def cut_trak(atrak, sample, data_offset_change):
  870. stbl = atrak.mdia.minf.stbl
  871. chunk = find_chunknum_stsc(stbl.stsc.table, sample)
  872. # print ('cutting trak: %r @ sample %d [chnk %d]' %
  873. # (atrak._atom, sample, chunk))
  874. media_time_diff = find_mediatime_stts(stbl.stts.table, sample) # - 0
  875. new_media_duration = atrak.mdia.mdhd.duration - media_time_diff
  876. """
  877. cut_stco64()
  878. cut_stsc()
  879. cut_stsz2()
  880. cut_sctts(stts)
  881. cut_sctts(ctts)
  882. cut_stss()
  883. """
  884. stco64 = stbl.stco or stbl.co64
  885. stsz2 = stbl.stsz or stbl.stz2
  886. new_stco64_t, new_stsc_t = cut_stco64_stsc(stco64.table, stbl.stsc.table,
  887. stsz2.table, chunk, sample,
  888. data_offset_change)
  889. new_stco64 = stco64.copy(table=new_stco64_t)
  890. new_stsc = stbl.stsc.copy(table=new_stsc_t)
  891. new_stsz2 = stsz2.copy(table=cut_stsz2(stsz2.table, sample))
  892. new_stts = stbl.stts.copy(table=cut_sctts(stbl.stts.table, sample))
  893. new_ctts = None
  894. if stbl.ctts:
  895. new_ctts = stbl.ctts.copy(table=cut_sctts(stbl.ctts.table, sample))
  896. new_stss = None
  897. if stbl.stss:
  898. new_stss = stbl.stss.copy(table=cut_stss(stbl.stss.table, sample))
  899. """
  900. new_mdhd = atrak.mdia.mdhd.copy()
  901. new_minf = atrak.mdia.minf.copy()
  902. new_mdia = atrak.mdia.copy()
  903. new_trak = atrak.copy()
  904. """
  905. stbl_attribs = dict(stts=new_stts, stsc=new_stsc)
  906. stbl_attribs[stbl.stco and 'stco' or 'co64'] = new_stco64
  907. stbl_attribs[stbl.stsz and 'stsz' or 'stz2'] = new_stsz2
  908. if new_ctts:
  909. stbl_attribs['ctts'] = new_ctts
  910. if new_stss:
  911. stbl_attribs['stss'] = new_stss
  912. new_stbl = stbl.copy(**stbl_attribs)
  913. new_minf = atrak.mdia.minf.copy(stbl=new_stbl)
  914. new_mdhd = atrak.mdia.mdhd.copy(duration=new_media_duration)
  915. new_mdia = atrak.mdia.copy(mdhd=new_mdhd, minf=new_minf)
  916. new_tkhd = atrak.tkhd.copy()
  917. new_trak = atrak.copy(tkhd=new_tkhd, mdia=new_mdia)
  918. # print 'old trak:'
  919. # print atrak
  920. return new_trak
  921. def update_offsets(atrak, data_offset_change):
  922. """
  923. cut_stco64(stco64, 1, ...) # again, after calculating new size of moov
  924. atrak.mdia.mdhd.duration = new_duration
  925. """
  926. # print 'offset updates:'
  927. # print atrak
  928. stbl = atrak.mdia.minf.stbl
  929. stco64 = stbl.stco or stbl.co64
  930. stco64.table = cut_stco64(stco64.table, 1, data_offset_change)
  931. # print atrak
  932. # print
  933. def cut_moov(amoov, t):
  934. ts = amoov.mvhd.timescale
  935. duration = amoov.mvhd.duration
  936. if t * ts >= duration:
  937. raise RuntimeError('Exceeded file duration: %r' %
  938. (duration / float(ts)))
  939. traks = amoov.trak
  940. # print 'movie timescale: %d, num tracks: %d' % (ts, len(traks))
  941. # print
  942. cut_info = map(lambda a: find_cut_trak_info(a, t), traks)
  943. # print 'cut_info:', cut_info
  944. new_data_offset = min([ci[3] for ci in cut_info])
  945. zero_offset = min([ci[2] for ci in cut_info])
  946. # print 'new offset: %d, delta: %d' % (new_data_offset,
  947. # new_data_offset - zero_offset)
  948. new_traks = map(lambda a, ci: cut_trak(a, ci[0],
  949. new_data_offset - zero_offset),
  950. traks, cut_info)
  951. new_moov = amoov.copy(mvhd=amoov.mvhd.copy(), trak=new_traks)
  952. moov_size_diff = amoov.get_size() - new_moov.get_size()
  953. # print ('moov_size_diff', moov_size_diff, amoov.get_size(),
  954. # new_moov.get_size())
  955. # print 'real moov sizes', amoov._atom.size, new_moov._atom.size
  956. # print 'new mdat start', zero_offset - moov_size_diff - 8
  957. def update_trak_duration(atrak):
  958. amdhd = atrak.mdia.mdhd
  959. new_duration = amdhd.duration * ts // amdhd.timescale # ... different
  960. # rounding? :/
  961. atrak.tkhd.duration = new_duration
  962. # print
  963. map(update_trak_duration, new_traks)
  964. map(lambda a: update_offsets(a, moov_size_diff), new_traks)
  965. return new_moov, new_data_offset - zero_offset, new_data_offset
  966. def split_atoms(f, out_f, t):
  967. aftype, amoov, alist = read_iso_file(f)
  968. t = find_nearest_syncpoint(amoov, t)
  969. # print 'nearest syncpoint:', t
  970. nmoov, delta, new_offset = cut_moov(amoov, t)
  971. write_split_header(out_f, nmoov, alist, delta)
  972. return new_offset
  973. def update_mdat_atoms(alist, size_delta):
  974. updated = []
  975. to_remove = size_delta
  976. pos = alist[0].offset
  977. for a in alist:
  978. data_size = a.size - a.head_size()
  979. size_change = min(data_size, to_remove)
  980. if size_change > 0:
  981. to_remove -= size_change
  982. new_size = real_size = a.size - size_change
  983. if a.real_size == 1:
  984. real_size = 1
  985. updated.append(atoms.Atom(new_size, 'mdat', pos, a.f,
  986. real_size=real_size))
  987. if to_remove == 0:
  988. break
  989. pos += new_size
  990. return updated
  991. def write_split_header(out_f, amoov, alist, size_delta):
  992. moov_idx = find_atom(alist, 'moov')
  993. mdat_idx = find_atom(alist, 'mdat')
  994. mdat = alist[mdat_idx]
  995. cut_offset = mdat.offset + mdat.head_size() + size_delta
  996. to_update = [a for a in alist[mdat_idx:] if a.offset < cut_offset]
  997. if [a for a in to_update if a.type != 'mdat']:
  998. raise FormatError('"mdat" and non-"mdat" (to-update) atoms mixed')
  999. updated_mdats = update_mdat_atoms(to_update, size_delta)
  1000. alist[moov_idx] = amoov
  1001. write_atoms(alist[:mdat_idx], out_f)
  1002. for a in updated_mdats:
  1003. write_ulong(out_f, a.real_size)
  1004. write_fcc(out_f, a.type)
  1005. if a.real_size == 1:
  1006. write_ulonglong(out_f, a.size)
  1007. def split(f, t, out_f=None):
  1008. wf = out_f
  1009. if wf is None:
  1010. from cStringIO import StringIO
  1011. wf = StringIO()
  1012. new_offset = split_atoms(f, wf, t)
  1013. return wf, new_offset
  1014. def split_and_write(in_f, out_f, t):
  1015. header_f, new_offset = split(in_f, t)
  1016. header_f.seek(0)
  1017. out_f.write(header_f.read())
  1018. in_f.seek(new_offset)
  1019. out_f.write(in_f.read())
  1020. def main(f, t):
  1021. split_and_write(f, file('/tmp/t.mp4', 'w'), t)
  1022. def find_sync_points(amoov):
  1023. ts = amoov.mvhd.timescale
  1024. traks = amoov.trak
  1025. def find_sync_samples(a):
  1026. stbl = a.mdia.minf.stbl
  1027. if not stbl.stss:
  1028. return []
  1029. stss = stbl.stss
  1030. stts = stbl.stts.table
  1031. ts = float(a.mdia.mdhd.timescale)
  1032. return map(lambda mt: mt / ts, find_mediatimes(stts, stss.table))
  1033. sync_tables = [t for t in map(find_sync_samples, traks) if t]
  1034. if sync_tables:
  1035. # ideally there should be only one sync table (from a video
  1036. # trak) - an arbitrary one will be taken otherwise...
  1037. return sync_tables[0]
  1038. return []
  1039. def find_nearest_syncpoint(amoov, t):
  1040. syncs = find_sync_points(amoov)
  1041. if not syncs:
  1042. # hardcoding duration - 0.1 sec as the farthest seek pos for now...
  1043. max_ts = amoov.mvhd.duration / float(amoov.mvhd.timescale) - 0.1
  1044. return max(0, min(t, max_ts))
  1045. found = 0
  1046. other = 0
  1047. for ss in syncs:
  1048. if ss > t:
  1049. other = ss
  1050. break
  1051. found = ss
  1052. if (abs(t - found) < abs(other - t)):
  1053. return found
  1054. return other
  1055. def get_nearest_syncpoint(f, t):
  1056. aftyp, amoov, alist = read_iso_file(f)
  1057. print find_nearest_syncpoint(amoov, t)
  1058. def get_sync_points(f):
  1059. aftyp, amoov, alist = read_iso_file(f)
  1060. return find_sync_points(amoov)
  1061. def get_debugging(f):
  1062. aftyp, amoov, alist = read_iso_file(f)
  1063. ts = amoov.mvhd.timescale
  1064. print aftyp
  1065. traks = amoov.trak
  1066. from pprint import pprint
  1067. pprint(map(lambda a: a.mdia.minf.stbl.stco, traks))
  1068. def change_chunk_offsets(amoov, data_offset):
  1069. """
  1070. @param data_offset: number of bytes to add to chunk offsets in all
  1071. traks of amoov
  1072. @type data_offset: int
  1073. """
  1074. # FIXME: make the offset direction sane in update_offsets...?
  1075. map(lambda a: update_offsets(a, - data_offset), amoov.trak)
  1076. def move_header_to_front(f):
  1077. aftype, amoov, alist = read_iso_file(f)
  1078. moov_idx = find_atom(alist, 'moov')
  1079. mdat_idx = find_atom(alist, 'mdat')
  1080. if moov_idx < mdat_idx:
  1081. # nothing to be done
  1082. return None
  1083. adict = atoms.atoms_dict(alist)
  1084. mdat = alist[mdat_idx]
  1085. new_moov_idx = mdat_idx
  1086. if 'wide' in adict:
  1087. # if 'wide' atom preceeds 'mdat', let's keep it that way
  1088. for wide in adict['wide']:
  1089. if wide.offset + wide.size == mdat.offset:
  1090. new_moov_idx -= 1
  1091. break
  1092. # for the moment assuming rewriting offsets in moov won't change
  1093. # the atoms sizes - could happen if:
  1094. # 2**32 - 1 - last_chunk_offset < moov.size
  1095. data_offset = amoov.get_size()
  1096. change_chunk_offsets(amoov, data_offset)
  1097. del alist[moov_idx]
  1098. alist[new_moov_idx:new_moov_idx] = [amoov]
  1099. return alist
  1100. def move_header_and_write(in_f, out_f):
  1101. alist = move_header_to_front(in_f)
  1102. if alist:
  1103. write_atoms(alist, out_f)
  1104. return True
  1105. return False
  1106. if __name__ == '__main__':
  1107. import sys
  1108. f = file(sys.argv[1])
  1109. if len(sys.argv) > 2:
  1110. t = float(sys.argv[2])
  1111. main(f, t)
  1112. # get_nearest_syncpoint(f, t)
  1113. else:
  1114. print get_sync_points(f)
  1115. # get_debugging(f)